From c34634b39a0b25917825da2656d60dc5462d010f Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 29 Apr 2016 15:06:37 +0200
Subject: [PATCH 001/261] Fixing load_dump command
---
taiga/export_import/management/commands/load_dump.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index a1d919f0..08f7811b 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -21,8 +21,8 @@ from django.db.models import signals
from optparse import make_option
from taiga.base.utils import json
-from taiga.export_import.import services
-from taiga.export_import.exceptions as err
+from taiga.export_import import services
+from taiga.export_import import exceptions as err
from taiga.export_import.renderers import ExportRenderer
from taiga.projects.models import Project
from taiga.users.models import User
From 66d5c372b275142cb8ca214889c83100c59a9d49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Sat, 30 Apr 2016 16:23:58 +0200
Subject: [PATCH 002/261] Improve projects admin panel
---
taiga/projects/admin.py | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 18bca9c5..44a2b412 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -16,6 +16,9 @@
# along with this program. If not, see .
from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.utils.html import format_html
+from django.utils.translation import ugettext_lazy as _
from taiga.projects.milestones.admin import MilestoneInline
from taiga.projects.notifications.admin import NotifyPolicyInline
@@ -67,18 +70,25 @@ class MembershipInline(admin.TabularInline):
class ProjectAdmin(admin.ModelAdmin):
list_display = ["id", "name", "slug", "is_private",
- "is_featured", "is_looking_for_people",
- "owner", "created_date"]
-
+ "owner_url", "blocked_code", "is_featured"]
list_display_links = ["id", "name", "slug"]
- list_filter = ("is_private", "is_featured", "is_looking_for_people")
- list_editable = ["is_featured"]
+ list_filter = ("is_private", "blocked_code", "is_featured")
+ list_editable = ["is_featured", "blocked_code"]
search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"]
inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline]
# NOTE: TextArrayField with a choices is broken in the admin panel.
exclude = ("anon_permissions", "public_permissions")
+ def owner_url(self, obj):
+ if obj.owner:
+ url = reverse('admin:{0}_{1}_change'.format(obj.owner._meta.app_label,
+ obj.owner._meta.model_name),
+ args=(obj.owner.pk,))
+ return format_html("{user}", url=url, user=obj.owner)
+ return ""
+ owner_url.short_description = _('owner')
+
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
return self.obj
From 8cd6c280106c777bb9c709d259e45b66c8172a09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Sun, 1 May 2016 19:10:28 +0200
Subject: [PATCH 003/261] [i18n] Update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/de/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/en/LC_MESSAGES/django.po | 190 +++++++++------
taiga/locale/es/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/fi/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/fr/LC_MESSAGES/django.po | 271 +++++++++++++--------
taiga/locale/it/LC_MESSAGES/django.po | 194 +++++++++------
taiga/locale/nl/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/pl/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/ru/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/sv/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/tr/LC_MESSAGES/django.po | 192 +++++++++------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 192 +++++++++------
14 files changed, 1641 insertions(+), 1126 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 9deffb7e..d643b4f8 100644
--- a/taiga/locale/ca/LC_MESSAGES/django.po
+++ b/taiga/locale/ca/LC_MESSAGES/django.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -188,7 +188,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -349,7 +349,7 @@ msgid "Error in filter params types."
msgstr ""
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr ""
@@ -480,62 +480,6 @@ msgstr "Es necessita arxiu dump."
msgid "Invalid dump format"
msgstr "Format d'arxiu dump invàlid"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr ""
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr ""
@@ -555,14 +499,103 @@ msgstr "Conté camps personalitzats invàlids."
msgid "Name duplicated for the project"
msgstr ""
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr ""
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr ""
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr ""
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr ""
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr ""
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr ""
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr ""
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr ""
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr ""
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr ""
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr ""
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr ""
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr ""
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr ""
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr ""
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1148,6 +1181,15 @@ msgstr "Administrar valors de projecte"
msgid "Admin roles"
msgstr "Administrar rols"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "Amo"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Arguments incomplets."
@@ -1194,14 +1236,6 @@ msgstr ""
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "Amo"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2359,39 +2393,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Rols"
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index d38d46fd..7b75e1f9 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/LC_MESSAGES/django.po
@@ -17,8 +17,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -218,7 +218,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -381,7 +381,7 @@ msgid "Error in filter params types."
msgstr "Fehler in Filter Parameter Typen."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
@@ -535,62 +535,6 @@ msgstr "Exportdatei erforderlich"
msgid "Invalid dump format"
msgstr "Ungültiges Exportdatei Format"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "Fehler beim Importieren der Projektdaten"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "Fehler beim Importieren der Listen von Projektattributen"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte "
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "Fehler beim Importieren der Kundenattribute"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "Fehler beim Importieren der Rollen"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "Fehler beim Importieren der Mitgliedschaften"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "Fehler beim Import der Sprints"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "Fehler beim Importieren von Wiki Seiten"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "Fehler beim Importieren von Wiki Links"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "Fehler beim Importieren der Tickets"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "Fehler beim Importieren der User-Stories"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "Fehler beim Importieren der Aufgaben"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "Fehler beim Importieren der Schlagworte"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "Fehler beim Importieren der Chroniken"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden"
@@ -610,14 +554,103 @@ msgstr "Enthält ungültige Benutzerfelder."
msgid "Name duplicated for the project"
msgstr "Der Name für das Projekt ist doppelt vergeben"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "Fehler beim Importieren der Projektdaten"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "Fehler beim Importieren der Rollen"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "Fehler beim Importieren der Mitgliedschaften"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "Fehler beim Importieren der Listen von Projektattributen"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte "
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "Fehler beim Importieren der Kundenattribute"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "Fehler beim Import der Sprints"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "Fehler beim Importieren der User-Stories"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "Fehler beim Importieren der Aufgaben"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "Fehler beim Importieren der Tickets"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "Fehler beim Importieren von Wiki Seiten"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "Fehler beim Importieren von Wiki Links"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "Fehler beim Importieren der Schlagworte"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "Fehler beim Importieren der Chroniken"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Fehler beim Erzeugen der Projekt Export-Datei "
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Fehler beim Laden von Projekt Export-Datei"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1338,6 +1371,15 @@ msgstr "Administrator Projekt Werte"
msgid "Admin roles"
msgstr "Administrator-Rollen"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "Besitzer"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Unvollständige Argumente"
@@ -1384,14 +1426,6 @@ msgstr "Teil-Aktualisierungen sind nicht unterstützt"
msgid "Project ID not matches between object and project"
msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "Besitzer"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2831,39 +2865,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Rollen"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 866f8483..4cde7d23 100644
--- a/taiga/locale/en/LC_MESSAGES/django.po
+++ b/taiga/locale/en/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -180,7 +180,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -341,7 +341,7 @@ msgid "Error in filter params types."
msgstr ""
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr ""
@@ -469,62 +469,6 @@ msgstr ""
msgid "Invalid dump format"
msgstr ""
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr ""
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr ""
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr ""
@@ -544,14 +488,103 @@ msgstr ""
msgid "Name duplicated for the project"
msgstr ""
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr ""
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr ""
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr ""
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr ""
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr ""
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr ""
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr ""
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr ""
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr ""
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr ""
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr ""
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr ""
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr ""
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr ""
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr ""
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1121,6 +1154,15 @@ msgstr ""
msgid "Admin roles"
msgstr ""
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr ""
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr ""
@@ -1167,14 +1209,6 @@ msgstr ""
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr ""
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2326,39 +2360,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index 7538d688..ec287179 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -16,8 +16,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -205,7 +205,7 @@ msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada.
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr "Elemento bloqueado"
@@ -369,7 +369,7 @@ msgid "Error in filter params types."
msgstr "Error en los típos de parámetros de filtrado"
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
@@ -522,62 +522,6 @@ msgstr "Se necesita el fichero con los datos exportados"
msgid "Invalid dump format"
msgstr "Formato de fichero de exportación inválido"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "error importando los datos del proyecto"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "error importando la listados de valores de attributos del proyecto"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "error importando los valores por defecto de los atributos del proyecto"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "error importando los atributos personalizados"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "error importando los roles"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "error importando los miembros"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "error importando los sprints"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "error importando las páginas del wiki"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "error importando los enlaces del wiki"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "error importando las peticiones"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "error importando las historias de usuario"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "error importando las tareas"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "error importando las etiquetas"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "error importando los timelines"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" no se ha encontrado en este proyecto"
@@ -597,14 +541,103 @@ msgstr "Contiene attributos personalizados inválidos."
msgid "Name duplicated for the project"
msgstr "Nombre duplicado para el proyecto"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "error importando los datos del proyecto"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "error importando los roles"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "error importando los miembros"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "error importando la listados de valores de attributos del proyecto"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "error importando los valores por defecto de los atributos del proyecto"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "error importando los atributos personalizados"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "error importando los sprints"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "error importando las historias de usuario"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "error importando las tareas"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "error importando las peticiones"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "error importando las páginas del wiki"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "error importando los enlaces del wiki"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "error importando las etiquetas"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "error importando los timelines"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Erro generando el volcado de datos del proyecto"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Error cargando el volcado de datos del proyecto"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1318,6 +1351,15 @@ msgstr "Administrar valores de proyecto"
msgid "Admin roles"
msgstr "Administrar roles"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "Dueño"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
@@ -1366,14 +1408,6 @@ msgstr "La actualización parcial no está soportada."
msgid "Project ID not matches between object and project"
msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "Dueño"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2766,39 +2800,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Roles"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 42a4be30..4034a724 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/LC_MESSAGES/django.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -190,7 +190,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -351,7 +351,7 @@ msgid "Error in filter params types."
msgstr "Error in filter params types."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
@@ -505,62 +505,6 @@ msgstr "Tarvitaan tiedosto"
msgid "Invalid dump format"
msgstr "Virheellinen tiedostomuoto"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "virhe projektidatan tuonnissa"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "virhe atribuuttilistan tuonnissa"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "virhe oletusarvojen tuonnissa"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "virhe omien arvojen tuonnissa"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "virhe roolien tuonnissa"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "virhe jäsenyyksien tuonnissa"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "virhe kierroksien tuonnissa"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "virhe wiki-sivujen tuonnissa"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "virhe viki-linkkien tuonnissa"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "virhe pyyntöjen tuonnissa"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "virhe käyttäjätarinoiden tuonnissa"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "virhe tehtävien tuonnissa"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "virhe avainsanojen sisäänlukemisessa"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "virhe aikajanojen tuonnissa"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" ei löytynyt tästä projektista"
@@ -580,14 +524,103 @@ msgstr "Sisältää vieheellisiä omia kenttiä."
msgid "Name duplicated for the project"
msgstr "Nimi on tuplana projektille"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "virhe projektidatan tuonnissa"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "virhe roolien tuonnissa"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "virhe jäsenyyksien tuonnissa"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "virhe atribuuttilistan tuonnissa"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "virhe oletusarvojen tuonnissa"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "virhe omien arvojen tuonnissa"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "virhe kierroksien tuonnissa"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "virhe käyttäjätarinoiden tuonnissa"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "virhe tehtävien tuonnissa"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "virhe pyyntöjen tuonnissa"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "virhe wiki-sivujen tuonnissa"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "virhe viki-linkkien tuonnissa"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "virhe avainsanojen sisäänlukemisessa"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "virhe aikajanojen tuonnissa"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Virhe tiedoston luonnissa"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Virhe tiedoston latauksessa"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1277,6 +1310,15 @@ msgstr "Hallinnoi projektin arvoja"
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "omistaja"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Puutteelliset argumentit"
@@ -1323,14 +1365,6 @@ msgstr ""
msgid "Project ID not matches between object and project"
msgstr "Projekti ID ei vastaa kohdetta ja projektia"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "omistaja"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2728,39 +2762,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Roolit"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index f84c878e..61fa72db 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -17,13 +17,14 @@
# Regis TEDONE , 2015
# Sébastien Talbot , 2016
# Stéphane Mor , 2015
+# Thierno Rignoux , 2016
# William Godin , 2015
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -214,7 +215,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr "Élément bloqué"
@@ -372,14 +373,14 @@ msgstr "Erreur de précondition"
#: taiga/base/exceptions.py:217
msgid "No room left for more projects."
-msgstr ""
+msgstr "Limite de projets atteinte."
#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Erreur dans les types de paramètres de filtres"
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
@@ -539,63 +540,6 @@ msgstr "Fichier de dump obligatoire"
msgid "Invalid dump format"
msgstr "Format de dump invalide"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "Erreur lors de l'importation de données"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "erreur lors de l'importation des listes des attributs de projet"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr ""
-"erreur lors de l'importation des valeurs par défaut des attributs de projet"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "Erreur à l'importation des champs personnalisés"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "Erreur à l'importation des rôles"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "Erreur à l'importation des groupes d'utilisateurs"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "Erreur lors de l'importation des sprints."
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "Erreur à l'importation des pages Wiki"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "Erreur à l'importation des liens Wiki"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "erreur à l'importation des problèmes"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "erreur à l'importation des histoires utilisateur"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "Erreur lors de l'importation des tâches."
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "erreur lors de l'importation des mots-clés"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "erreur lors de l'import des timelines"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" non trouvé dans the projet"
@@ -615,14 +559,104 @@ msgstr "Contient des champs personnalisés non valides."
msgid "Name duplicated for the project"
msgstr "Nom dupliqué pour ce projet"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "Erreur lors de l'importation de données"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "Erreur à l'importation des rôles"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "Erreur à l'importation des groupes d'utilisateurs"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "erreur lors de l'importation des listes des attributs de projet"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr ""
+"erreur lors de l'importation des valeurs par défaut des attributs de projet"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "Erreur à l'importation des champs personnalisés"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "Erreur lors de l'importation des sprints."
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "erreur à l'importation des histoires utilisateur"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "Erreur lors de l'importation des tâches."
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "erreur à l'importation des problèmes"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "Erreur à l'importation des pages Wiki"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "Erreur à l'importation des liens Wiki"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "erreur lors de l'importation des mots-clés"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "erreur lors de l'import des timelines"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Erreur dans la génération du dump du projet"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Erreur au chargement du dump du projet"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -838,6 +872,17 @@ msgid ""
"---\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"Hey %(user)s,\n"
+"\n"
+"Votre dump a été correctement importé.\n"
+"\n"
+"Vous pouvez voir le(s) %(project)s ici :\n"
+"\n"
+"%(url)s\n"
+"\n"
+"---\n"
+"L'équipe Taiga\n"
#: taiga/export_import/templates/emails/load_dump-subject.jinja:1
#, python-format
@@ -1290,6 +1335,15 @@ msgstr "Administrer les paramètres du projet"
msgid "Admin roles"
msgstr "Administrer les rôles"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "propriétaire"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "arguments manquants"
@@ -1316,13 +1370,15 @@ msgstr "L'utilisateur n'existe pas"
#: taiga/projects/api.py:366
msgid "The user must be already a project member"
-msgstr ""
+msgstr "L'utilisateur doit déjà être un membre du projet"
#: taiga/projects/api.py:672
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
+"Le projet doit avoir un propriétaire et au moins l'un de ses membres doit "
+"être un administrateur actif."
#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
@@ -1336,14 +1392,6 @@ msgstr "Mises à jour partielles non supportées"
msgid "Project ID not matches between object and project"
msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "propriétaire"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -1415,15 +1463,15 @@ msgstr "Talky"
#: taiga/projects/choices.py:32
msgid "This project is blocked due to payment failure"
-msgstr ""
+msgstr "Ce projet a été bloqué pour cause d'impayé"
#: taiga/projects/choices.py:33
msgid "This project is blocked by admin staff"
-msgstr ""
+msgstr "Ce projet a été bloqué par l'équipe administrative"
#: taiga/projects/choices.py:34
msgid "This project is blocked because the owner left"
-msgstr ""
+msgstr "Ce projet est bloqué car son propriétaire est parti"
#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
@@ -1842,7 +1890,7 @@ msgstr "couleurs des tags"
#: taiga/projects/models.py:221
msgid "project transfer token"
-msgstr ""
+msgstr "jeton de transfert de projet"
#: taiga/projects/models.py:225
msgid "blocked code"
@@ -2508,6 +2556,8 @@ msgstr "version"
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
+"Vous ne pouvez pas quitter le projet si vous en êtes le propriétaire ou "
+"qu'il n'y a pas d'autre administrateur."
#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
@@ -2519,77 +2569,78 @@ msgstr "Rôle non valide pour le projet"
#: taiga/projects/serializers.py:195
msgid "The project owner must be admin."
-msgstr ""
+msgstr "Le propriétaire du projet doit être un administrateur."
#: taiga/projects/serializers.py:198
msgid "At least one user must be an active admin for this project."
msgstr ""
+"Au moins un utilisateur doit être un administrateur actif de ce projet."
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Rôles"
#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
+msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets privés"
#: taiga/projects/services/members.py:120
msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
+msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets publics"
#: taiga/projects/services/projects.py:69
#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
msgid "You can't have more private projects"
-msgstr ""
+msgstr "Vous avez atteint le nombre maximum de projets privés"
#: taiga/projects/services/projects.py:73
#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
msgid ""
"This project reaches your current limit of memberships for private projects"
-msgstr ""
+msgstr "Ce projet privé est le dernier que vous pouvez rejoindre"
#: taiga/projects/services/projects.py:77
#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
msgid "You can't have more public projects"
-msgstr ""
+msgstr "Vous avez atteint le nombre maximum de projets publics."
#: taiga/projects/services/projects.py:81
#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for public projects"
-msgstr ""
+msgstr "Ce projet public est le dernier que vous pouvez rejoindre"
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
@@ -2608,7 +2659,7 @@ msgstr "Jeton invalide"
#: taiga/projects/services/transfer.py:66
msgid "Token has expired"
-msgstr ""
+msgstr "Le jeton est périmé"
#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
@@ -2653,6 +2704,11 @@ msgid ""
"Management Tool.
\n"
" "
msgstr ""
+"\n"
+"Vous avez été invité à Taiga !
\n"
+"Hey ! %(full_name)s vous a invité à rejoindre le projet %(project)s"
+"em>. Taiga est un outil de gestion de projet Agile libre et open source."
+"
"
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:17
#, python-format
@@ -2784,7 +2840,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
#, python-format
msgid "%(new_owner_name)s says:
"
-msgstr ""
+msgstr "%(new_owner_name)s a dit :
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -2793,6 +2849,10 @@ msgid ""
"p>\n"
" "
msgstr ""
+"\n"
+"À partir de maintenant, votre nouveau status pour ce projet sera celui "
+"d'Administrateur.
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -2813,6 +2873,9 @@ msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
+"\n"
+"À partir de maintenant, votre nouveau status pour ce projet sera celui "
+"d'Administrateur.\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
@@ -2822,6 +2885,8 @@ msgid ""
"\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"L'Équipe Taiga\n"
#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
#, python-format
@@ -2847,6 +2912,8 @@ msgid ""
" %(rejecter_name)s says:
\n"
" "
msgstr ""
+"\n"
+" %(rejecter_name)s a dit :
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
@@ -2855,6 +2922,10 @@ msgid ""
"different person.\n"
" "
msgstr ""
+"\n"
+" Vous pouvez toujours, si vous le désirez, essayer de transférer la "
+"propriété du projet à quelqu'un d'autre.
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
@@ -2914,7 +2985,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
msgid "Continue"
-msgstr ""
+msgstr "Continuer"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
#, python-format
@@ -2960,6 +3031,8 @@ msgid ""
" %(owner_name)s says:
\n"
" "
msgstr ""
+"\n"
+" %(owner_name)s a dit:
"
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
@@ -3344,7 +3417,7 @@ msgstr ""
#: taiga/users/admin.py:49
msgid "id"
-msgstr ""
+msgstr "id"
#: taiga/users/admin.py:81
msgid "Project Ownership"
@@ -3364,7 +3437,7 @@ msgstr "Permissions"
#: taiga/users/admin.py:123
msgid "Restrictions"
-msgstr ""
+msgstr "Restrictions"
#: taiga/users/admin.py:125
msgid "Important dates"
@@ -3674,7 +3747,7 @@ msgstr ""
"\n"
"Merci pour votre inscription sur Taiga\n"
"\n"
-"Nous espérons que vous l'appréciez\n"
+"Nous espérons que vous l'apprécierez\n"
"\n"
"Nous avons construit Taiga car nous voulions que l'outil de gestion de "
"projet que nous utilisons au quotidien, nous rappelle en permanence pourquoi "
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index c1f4fcd6..8090f674 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -15,8 +15,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -201,7 +201,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -365,7 +365,7 @@ msgid "Error in filter params types."
msgstr "Errore nel filtro del tipo di parametri."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
@@ -531,63 +531,6 @@ msgstr "E' richiesto un file di dump"
msgid "Invalid dump format"
msgstr "Formato di dump invalido"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "Errore nell'importazione del progetto dati"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "Errore nell'importazione della lista degli attributi di progetto"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr ""
-"Errore nell'importazione dei valori predefiniti degli attributi del progetto."
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "Errore nell'importazione degli attributi personalizzati"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "Errore nell'importazione i ruoli"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "Errore nell'importazione delle iscrizioni"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "errore nell'importazione degli sprints"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "Errore nell'importazione delle pagine wiki"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "Errore nell'importazione dei link di wiki"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "errore nell'importazione dei problemi"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "Errore nell'importazione delle user story"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "Errore nell'importazione dei compiti "
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "Errore nell'importazione dei tags"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "Errore nell'importazione delle timelines"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" non è stato trovato in questo progetto"
@@ -607,14 +550,104 @@ msgstr "Contiene campi personalizzati invalidi."
msgid "Name duplicated for the project"
msgstr "Il nome del progetto è duplicato"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "Errore nell'importazione del progetto dati"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "Errore nell'importazione i ruoli"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "Errore nell'importazione delle iscrizioni"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "Errore nell'importazione della lista degli attributi di progetto"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr ""
+"Errore nell'importazione dei valori predefiniti degli attributi del progetto."
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "Errore nell'importazione degli attributi personalizzati"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "errore nell'importazione degli sprints"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "Errore nell'importazione delle user story"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "Errore nell'importazione dei compiti "
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "errore nell'importazione dei problemi"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "Errore nell'importazione delle pagine wiki"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "Errore nell'importazione dei link di wiki"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "Errore nell'importazione dei tags"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "Errore nell'importazione delle timelines"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Errore nella creazione del dump di progetto"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Errore nel caricamento del dump di progetto"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1413,6 +1446,15 @@ msgstr "Valori dell'amministratore del progetto"
msgid "Admin roles"
msgstr "Ruoli dell'amministratore"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "proprietario"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argomento non valido"
@@ -1459,14 +1501,6 @@ msgstr "Aggiornamento non parziale non supportato"
msgid "Project ID not matches between object and project"
msgstr "L'ID di progetto non corrisponde tra oggetto e progetto"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "proprietario"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -3005,39 +3039,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Ruoli"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index d1782718..e0d6070d 100644
--- a/taiga/locale/nl/LC_MESSAGES/django.po
+++ b/taiga/locale/nl/LC_MESSAGES/django.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -199,7 +199,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -361,7 +361,7 @@ msgid "Error in filter params types."
msgstr "Fout in filter params types."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
@@ -518,62 +518,6 @@ msgstr "Dump file nodig"
msgid "Invalid dump format"
msgstr "Ongeldig dump formaat"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "fout bij het importeren van project data"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "fout bij importeren van project attributenlijst"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "fout bij importeren van standaard projectattributen waarden"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "fout bij importeren eigen attributen"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "fout bij importeren rollen"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "fout bij importeren lidmaatschappen"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "fout bij importeren sprints"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "fout bij importeren wiki pagina's"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "fout bij importeren wiki links"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "fout bij importeren issues"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "fout bij importeren user stories"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "fout bij importeren taken"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "fout bij importeren tags"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "fout bij importeren tijdlijnen"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" niet gevonden in dit project"
@@ -593,14 +537,103 @@ msgstr "Het bevat ongeldige eigen velden:"
msgid "Name duplicated for the project"
msgstr "Naam gedupliceerd voor het project"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "fout bij het importeren van project data"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "fout bij importeren rollen"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "fout bij importeren lidmaatschappen"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "fout bij importeren van project attributenlijst"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "fout bij importeren van standaard projectattributen waarden"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "fout bij importeren eigen attributen"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "fout bij importeren sprints"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "fout bij importeren user stories"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "fout bij importeren taken"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "fout bij importeren issues"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "fout bij importeren wiki pagina's"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "fout bij importeren wiki links"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "fout bij importeren tags"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "fout bij importeren tijdlijnen"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Fout bij genereren project dump"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Fout bij laden project dump"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1211,6 +1244,15 @@ msgstr "Admin project waarden"
msgid "Admin roles"
msgstr "Admin rollen"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "eigenaar"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Onvolledige argumenten"
@@ -1257,14 +1299,6 @@ msgstr ""
msgid "Project ID not matches between object and project"
msgstr "Project ID van object is niet gelijk aan die van het project"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "eigenaar"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2448,39 +2482,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Rollen"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 5ba1251c..f7ce189e 100644
--- a/taiga/locale/pl/LC_MESSAGES/django.po
+++ b/taiga/locale/pl/LC_MESSAGES/django.po
@@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -193,7 +193,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -356,7 +356,7 @@ msgid "Error in filter params types."
msgstr "Błąd w parametrach typów filtrów."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
@@ -519,62 +519,6 @@ msgstr "Wymagany plik zrzutu"
msgid "Invalid dump format"
msgstr "Nieprawidłowy format zrzutu"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "błąd w trakcie importu danych projektu"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "błąd w trakcie importu atrybutów projektu"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "błąd w trakcie importu domyślnych atrybutów projektu"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "błąd w trakcie importu niestandardowych atrybutów"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "błąd w trakcie importu ról"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "błąd w trakcie importu członkostw"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "błąd w trakcie importu sprintów"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "błąd w trakcie importu stron Wiki"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "błąd w trakcie importu linków Wiki"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "błąd w trakcie importu zgłoszeń"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "błąd w trakcie importu historyjek użytkownika"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "błąd w trakcie importu zadań"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "błąd w trakcie importu tagów"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "błąd w trakcie importu osi czasu"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" nie odnaleziono w projekcie"
@@ -594,14 +538,103 @@ msgstr "Zawiera niewłaściwe pola niestandardowe."
msgid "Name duplicated for the project"
msgstr "Nazwa projektu zduplikowana"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "błąd w trakcie importu danych projektu"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "błąd w trakcie importu ról"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "błąd w trakcie importu członkostw"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "błąd w trakcie importu atrybutów projektu"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "błąd w trakcie importu domyślnych atrybutów projektu"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "błąd w trakcie importu niestandardowych atrybutów"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "błąd w trakcie importu sprintów"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "błąd w trakcie importu historyjek użytkownika"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "błąd w trakcie importu zadań"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "błąd w trakcie importu zgłoszeń"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "błąd w trakcie importu stron Wiki"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "błąd w trakcie importu linków Wiki"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "błąd w trakcie importu tagów"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "błąd w trakcie importu osi czasu"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Błąd w trakcie generowania zrzutu projektu"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Błąd w trakcie wczytywania zrzutu projektu"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1319,6 +1352,15 @@ msgstr "Administruj wartościami projektu"
msgid "Admin roles"
msgstr "Administruj rolami"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "właściciel"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Pola niekompletne"
@@ -1365,14 +1407,6 @@ msgstr ""
msgid "Project ID not matches between object and project"
msgstr "ID nie pasuje pomiędzy obiektem a projektem"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "właściciel"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2785,39 +2819,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Role"
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 9b3d2b72..2e440979 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -19,8 +19,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
@@ -200,7 +200,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -363,7 +363,7 @@ msgid "Error in filter params types."
msgstr "Erro nos tipos de parâmetros do filtro."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
@@ -527,62 +527,6 @@ msgstr "Necessário de arquivo de restauração"
msgid "Invalid dump format"
msgstr "Formato de aquivo de restauração inválido"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "erro ao importar informações de projeto"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "erro importando lista de atributos do projeto"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "erro importando valores de atributos do projeto padrão"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "erro importando atributos personalizados"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "erro importando funcões"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "erro importando filiações"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "erro importando sprints"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "erro importando páginas wiki"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "erro importando wiki links"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "erro importando casos"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "erro importando user stories"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "erro importando tarefas"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "erro importando tags"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "erro importando linha do tempo"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" não encontrado nesse projeto"
@@ -602,14 +546,103 @@ msgstr "Contém campos personalizados inválidos"
msgid "Name duplicated for the project"
msgstr "Nome duplicado para o projeto"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "erro ao importar informações de projeto"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "erro importando funcões"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "erro importando filiações"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "erro importando lista de atributos do projeto"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "erro importando valores de atributos do projeto padrão"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "erro importando atributos personalizados"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "erro importando sprints"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "erro importando user stories"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "erro importando tarefas"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "erro importando casos"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "erro importando páginas wiki"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "erro importando wiki links"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "erro importando tags"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "erro importando linha do tempo"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Erro gerando arquivo de restauração do projeto"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Erro carregando arquivo de restauração do projeto"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1325,6 +1358,15 @@ msgstr "Valores projeto admin"
msgid "Admin roles"
msgstr "Funções Admin"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "dono"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
@@ -1371,14 +1413,6 @@ msgstr "Atualizações parciais não são suportadas"
msgid "Project ID not matches between object and project"
msgstr "ID do projeto não combina entre objeto e projeto"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "dono"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2769,39 +2803,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Funções"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index f9c67e5f..366ee566 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -15,8 +15,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -203,7 +203,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -365,7 +365,7 @@ msgid "Error in filter params types."
msgstr "Ошибка в типах фильтров для параметров."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
@@ -529,62 +529,6 @@ msgstr "Необходим дамп-файл"
msgid "Invalid dump format"
msgstr "Неправильный формат дампа"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "ошибка при импорте данных проекта"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "ошибка при импорте списков свойств проекта"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "ошибка при импорте значений по умолчанию свойств проекта"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "ошибка при импорте пользовательских свойств"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "ошибка при импорте ролей"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "ошибка при импорте членства"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "ошибка при импорте спринтов"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "ошибка при импорте вики-страниц"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "ошибка при импорте вики-ссылок"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "ошибка при импорте запросов"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "ошибка импорта историй от пользователей"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "ошибка импорта задач"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "ошибка импорта тэгов"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "ошибка импорта хронологии проекта"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" не найдено в этом проекте"
@@ -604,14 +548,103 @@ msgstr "Содержит неверные специальные поля"
msgid "Name duplicated for the project"
msgstr "Уже есть такое имя для проекта"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "ошибка при импорте данных проекта"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "ошибка при импорте ролей"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "ошибка при импорте членства"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "ошибка при импорте списков свойств проекта"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "ошибка при импорте значений по умолчанию свойств проекта"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "ошибка при импорте пользовательских свойств"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "ошибка при импорте спринтов"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "ошибка импорта историй от пользователей"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "ошибка импорта задач"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "ошибка при импорте запросов"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "ошибка при импорте вики-страниц"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "ошибка при импорте вики-ссылок"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "ошибка импорта тэгов"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "ошибка импорта хронологии проекта"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Ошибка создания свалочного файла для проекта"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Ошибка загрузки свалочного файла проекта"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1326,6 +1359,15 @@ msgstr "Управлять значениями проекта"
msgid "Admin roles"
msgstr "Управлять ролями"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "владелец"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
@@ -1372,14 +1414,6 @@ msgstr "Частичные обновления не поддерживаютс
msgid "Project ID not matches between object and project"
msgstr "Идентификатор проекта не подходит к этому объекту"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "владелец"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2784,39 +2818,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Роли"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 88143ba9..705d5cc6 100644
--- a/taiga/locale/sv/LC_MESSAGES/django.po
+++ b/taiga/locale/sv/LC_MESSAGES/django.po
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -192,7 +192,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -355,7 +355,7 @@ msgid "Error in filter params types."
msgstr "Fel i filterparametertyper."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
@@ -503,62 +503,6 @@ msgstr "Behöver en hämtningsfil"
msgid "Invalid dump format"
msgstr "Invalid hämtningsfilformat"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "fel vid import av projektdata"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "fel vid import av en lista på projektegenskaper"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "fel vid import av standard projektegenskapsvärden"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "fel vid import av anpassade egenskaper"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "fel vid importering av roller"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "fel vid import av medlemskap"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "felaktig import av sprintar"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "vel vid import av wiki-sidor"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "fel vid import av wiki-länkar"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "fel vid import av ärenden"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "fel vid import av användarhistorier"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "fel vid import av uppgifter"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "fel vid importering av taggar"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "fel vid importering av tidslinje"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" gick inte att hitta för det här projektet"
@@ -578,14 +522,103 @@ msgstr "Innehåller felaktigt anpassad fält."
msgid "Name duplicated for the project"
msgstr "Namnet är upprepad för projektet"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "fel vid import av projektdata"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "fel vid importering av roller"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "fel vid import av medlemskap"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "fel vid import av en lista på projektegenskaper"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "fel vid import av standard projektegenskapsvärden"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "fel vid import av anpassade egenskaper"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "felaktig import av sprintar"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "fel vid import av användarhistorier"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "fel vid import av uppgifter"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "fel vid import av ärenden"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "vel vid import av wiki-sidor"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "fel vid import av wiki-länkar"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "fel vid importering av taggar"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "fel vid importering av tidslinje"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Fel vid skapandet av projektkopia"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Feil vid hämtning av projektkopia"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1166,6 +1199,15 @@ msgstr "Administrera projektvärden"
msgid "Admin roles"
msgstr "Administratorroller"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "ägare"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Felaktiga argument"
@@ -1212,14 +1254,6 @@ msgstr "Delvisa uppdateringar stöds inte. "
msgid "Project ID not matches between object and project"
msgstr "Projekt-ID stämmer inte mellan objekt och projekt"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "ägare"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2371,39 +2405,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Roller"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 59c3af70..15ea255e 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -200,7 +200,7 @@ msgstr ""
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr "Engellenmiş nesne"
@@ -361,7 +361,7 @@ msgid "Error in filter params types."
msgstr "Parametre tipleri filtresinde hata."
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
@@ -513,62 +513,6 @@ msgstr "İhtiyaç duyulan döküm dosyası"
msgid "Invalid dump format"
msgstr "Geçersiz döküm biçemi"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "İçeri aktarılan proje verisinde hata"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "özel öznitelikler içeri aktarılırken hata"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "İçeri aktarılan rollerde hata"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "İçeri aktarılan üyeliklerde hata"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "İçeri aktarılan sprintlerde hata"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "İçeri aktarılan wiki sayfalarında hata"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "İçeri aktarılan wiki bağlantılarında hata"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "İçeri aktarılan taleplerde hata"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "İçeri aktarılan görevlerde hata"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "İçeri aktarılan etiketlerde hata"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "zaman çizelgesi içeri aktarılırken hata"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" bu projede bulunamadı"
@@ -588,14 +532,103 @@ msgstr "Geçersiz özel alanlar içeriyor."
msgid "Name duplicated for the project"
msgstr "Aynı isimde proje bulunmakta"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "İçeri aktarılan proje verisinde hata"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "İçeri aktarılan rollerde hata"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "İçeri aktarılan üyeliklerde hata"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "özel öznitelikler içeri aktarılırken hata"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "İçeri aktarılan sprintlerde hata"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "İçeri aktarılan görevlerde hata"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "İçeri aktarılan taleplerde hata"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "İçeri aktarılan wiki sayfalarında hata"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "İçeri aktarılan wiki bağlantılarında hata"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "İçeri aktarılan etiketlerde hata"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "zaman çizelgesi içeri aktarılırken hata"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "Proje dökümü oluşturulurken hata"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "Proje dökümü yükleniyorken hata"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1273,6 +1306,15 @@ msgstr "Admin proje değerleri"
msgid "Admin roles"
msgstr "Yönetici rolleri"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "sahip"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Eksik parametreq"
@@ -1319,14 +1361,6 @@ msgstr "Kısmi güncellemeler desteklenmiyor"
msgid "Project ID not matches between object and project"
msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "sahip"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2544,39 +2578,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "Roller"
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index d357abfa..5537e07f 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,8 +11,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-19 16:00+0200\n"
-"PO-Revision-Date: 2016-04-19 14:00+0000\n"
+"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"PO-Revision-Date: 2016-05-01 17:09+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
@@ -186,7 +186,7 @@ msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:67
+#: taiga/webhooks/api.py:68
msgid "Blocked element"
msgstr ""
@@ -347,7 +347,7 @@ msgid "Error in filter params types."
msgstr "過濾參數類型出錯"
#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:59
+#: taiga/projects/filters.py:63
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
@@ -510,62 +510,6 @@ msgstr "需要的堆存檔案"
msgid "Invalid dump format"
msgstr "無效堆存格式"
-#: taiga/export_import/dump_service.py:112
-msgid "error importing project data"
-msgstr "滙入重要專案資料出錯"
-
-#: taiga/export_import/dump_service.py:125
-msgid "error importing lists of project attributes"
-msgstr "滙入標籤出錯"
-
-#: taiga/export_import/dump_service.py:130
-msgid "error importing default project attributes values"
-msgstr "滙入預設專案屬性數值出錯"
-
-#: taiga/export_import/dump_service.py:140
-msgid "error importing custom attributes"
-msgstr "滙入客制性屬出錯"
-
-#: taiga/export_import/dump_service.py:145
-msgid "error importing roles"
-msgstr "滙入角色出錯"
-
-#: taiga/export_import/dump_service.py:160
-msgid "error importing memberships"
-msgstr "滙入成員資格出錯"
-
-#: taiga/export_import/dump_service.py:165
-msgid "error importing sprints"
-msgstr "滙入衝刺任務出錯"
-
-#: taiga/export_import/dump_service.py:170
-msgid "error importing wiki pages"
-msgstr "滙入維基頁出錯"
-
-#: taiga/export_import/dump_service.py:175
-msgid "error importing wiki links"
-msgstr "滙入維基連結出錯"
-
-#: taiga/export_import/dump_service.py:180
-msgid "error importing issues"
-msgstr "滙入問題出錯"
-
-#: taiga/export_import/dump_service.py:185
-msgid "error importing user stories"
-msgstr "滙入使用者故事出錯"
-
-#: taiga/export_import/dump_service.py:190
-msgid "error importing tasks"
-msgstr "滙入任務出錯"
-
-#: taiga/export_import/dump_service.py:195
-msgid "error importing tags"
-msgstr "滙入標籤出錯"
-
-#: taiga/export_import/dump_service.py:199
-msgid "error importing timelines"
-msgstr "滙入時間軸出錯"
-
#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" 無法在此專案中找到"
@@ -585,14 +529,103 @@ msgstr "包括無效慣例欄位"
msgid "Name duplicated for the project"
msgstr "專案的名稱被複製了"
-#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
+#: taiga/export_import/services/store.py:621
+#: taiga/export_import/services/store.py:639
+msgid "error importing project data"
+msgstr "滙入重要專案資料出錯"
+
+#: taiga/export_import/services/store.py:646
+msgid "error importing roles"
+msgstr "滙入角色出錯"
+
+#: taiga/export_import/services/store.py:651
+msgid "error importing memberships"
+msgstr "滙入成員資格出錯"
+
+#: taiga/export_import/services/store.py:661
+msgid "error importing lists of project attributes"
+msgstr "滙入標籤出錯"
+
+#: taiga/export_import/services/store.py:665
+msgid "error importing default project attributes values"
+msgstr "滙入預設專案屬性數值出錯"
+
+#: taiga/export_import/services/store.py:674
+msgid "error importing custom attributes"
+msgstr "滙入客制性屬出錯"
+
+#: taiga/export_import/services/store.py:679
+msgid "error importing sprints"
+msgstr "滙入衝刺任務出錯"
+
+#: taiga/export_import/services/store.py:683
+msgid "error importing user stories"
+msgstr "滙入使用者故事出錯"
+
+#: taiga/export_import/services/store.py:687
+msgid "error importing tasks"
+msgstr "滙入任務出錯"
+
+#: taiga/export_import/services/store.py:691
+msgid "error importing issues"
+msgstr "滙入問題出錯"
+
+#: taiga/export_import/services/store.py:695
+msgid "error importing wiki pages"
+msgstr "滙入維基頁出錯"
+
+#: taiga/export_import/services/store.py:699
+msgid "error importing wiki links"
+msgstr "滙入維基連結出錯"
+
+#: taiga/export_import/services/store.py:703
+msgid "error importing tags"
+msgstr "滙入標籤出錯"
+
+#: taiga/export_import/services/store.py:707
+msgid "error importing timelines"
+msgstr "滙入時間軸出錯"
+
+#: taiga/export_import/services/store.py:731
+msgid "unexpected error importing project"
+msgstr ""
+
+#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
msgid "Error generating project dump"
msgstr "產生專案傾倒時出錯"
-#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
+#: taiga/export_import/tasks.py:81
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:110
msgid "Error loading project dump"
msgstr "載入專案傾倒時出錯"
+#: taiga/export_import/tasks.py:111
+msgid "Error loading your project dump file"
+msgstr ""
+
+#: taiga/export_import/tasks.py:125
+msgid " -- no detail info --"
+msgstr ""
+
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
msgid ""
@@ -1299,6 +1332,15 @@ msgstr "管理員專案數值"
msgid "Admin roles"
msgstr "管理員角色"
+#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
+#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
+#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
+#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
+#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
+#: taiga/userstorage/models.py:26
+msgid "owner"
+msgstr "所有者"
+
#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "不完整參數"
@@ -1345,14 +1387,6 @@ msgstr "不支援部份更新"
msgid "Project ID not matches between object and project"
msgstr "專案ID不符合物件與專案"
-#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
-#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
-#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
-#: taiga/users/admin.py:69 taiga/userstorage/models.py:26
-msgid "owner"
-msgstr "所有者"
-
#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
@@ -2754,39 +2788,39 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:392
+#: taiga/projects/serializers.py:396
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:393
+#: taiga/projects/serializers.py:397
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:398
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:399
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:400
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:401
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:402
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:403
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:404
msgid "Roles"
msgstr "角色"
From af3fedee426ad5a20fff47fadf6144d83563a9b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 3 May 2016 11:51:01 +0200
Subject: [PATCH 004/261] Make is_private editable in the admin panel
---
taiga/projects/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 44a2b412..4eb6d5d6 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -73,7 +73,7 @@ class ProjectAdmin(admin.ModelAdmin):
"owner_url", "blocked_code", "is_featured"]
list_display_links = ["id", "name", "slug"]
list_filter = ("is_private", "blocked_code", "is_featured")
- list_editable = ["is_featured", "blocked_code"]
+ list_editable = ["is_private", "is_featured", "blocked_code"]
search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"]
inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline]
From a8d52cce01811f49e69adf05db813b9e21279fda Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 3 May 2016 13:05:32 +0200
Subject: [PATCH 005/261] Create actions to make projects public or private
---
taiga/projects/admin.py | 43 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 41 insertions(+), 2 deletions(-)
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 4eb6d5d6..99757145 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -17,9 +17,11 @@
from django.contrib import admin
from django.core.urlresolvers import reverse
+from django.db import transaction
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
+from taiga.permissions import permissions
from taiga.projects.milestones.admin import MilestoneInline
from taiga.projects.notifications.admin import NotifyPolicyInline
from taiga.projects.likes.admin import LikeInline
@@ -73,7 +75,7 @@ class ProjectAdmin(admin.ModelAdmin):
"owner_url", "blocked_code", "is_featured"]
list_display_links = ["id", "name", "slug"]
list_filter = ("is_private", "blocked_code", "is_featured")
- list_editable = ["is_private", "is_featured", "blocked_code"]
+ list_editable = ["is_featured", "blocked_code"]
search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"]
inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline]
@@ -121,8 +123,45 @@ class ProjectAdmin(admin.ModelAdmin):
obj.delete_related_content()
super().delete_model(request, obj)
-# User Stories common admins
+ ## Actions
+ actions = [
+ "make_public",
+ "make_private"
+ ]
+ @transaction.atomic
+ def make_public(self, request, queryset):
+ total_updates = 0
+ for project in queryset.exclude(is_private=False):
+ project.is_private = False
+
+ anon_permissions = list(map(lambda perm: perm[0], permissions.ANON_PERMISSIONS))
+ project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions))
+ project.public_permissions = list(set((project.public_permissions or []) + anon_permissions))
+
+ project.save()
+ total_updates += 1
+
+ self.message_user(request, _("{count} successfully made public.").format(count=total_updates))
+ make_public.short_description = _("Make public")
+
+ @transaction.atomic
+ def make_private(self, request, queryset):
+ total_updates = 0
+
+ for project in queryset.exclude(is_private=True):
+ project.is_private = True
+ project.anon_permissions = []
+ project.public_permissions = []
+
+ project.save()
+ total_updates += 1
+
+ self.message_user(request, _("{count} successfully made private.").format(count=total_updates))
+ make_private.short_description = _("Make private")
+
+
+# User Stories common admins
class PointsAdmin(admin.ModelAdmin):
list_display = ["project", "order", "name", "value"]
list_display_links = ["name"]
From 8791d5d10343a332182260595bb7eb4e723a22bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 3 May 2016 20:57:44 +0200
Subject: [PATCH 006/261] Fix error importing user stories with related issues
---
taiga/export_import/services/store.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py
index e286c97c..fe34ff2a 100644
--- a/taiga/export_import/services/store.py
+++ b/taiga/export_import/services/store.py
@@ -673,11 +673,14 @@ def _populate_project_object(project, data):
serializers.IssueCustomAttributeExportSerializer)
check_if_there_is_some_error(_("error importing custom attributes"), project)
-
# Create milestones
store_milestones(project, data)
check_if_there_is_some_error(_("error importing sprints"), project)
+ # Create issues
+ store_issues(project, data)
+ check_if_there_is_some_error(_("error importing issues"), project)
+
# Create user stories
store_user_stories(project, data)
check_if_there_is_some_error(_("error importing user stories"), project)
@@ -686,10 +689,6 @@ def _populate_project_object(project, data):
store_tasks(project, data)
check_if_there_is_some_error(_("error importing tasks"), project)
- # Create issues
- store_issues(project, data)
- check_if_there_is_some_error(_("error importing issues"), project)
-
# Create wiki pages
store_wiki_pages(project, data)
check_if_there_is_some_error(_("error importing wiki pages"), project)
From 2456c0454de97561fff5b410ba29753cf1911d39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Tue, 3 May 2016 23:03:24 +0200
Subject: [PATCH 007/261] Improve the project admin
---
taiga/projects/admin.py | 57 +++++++++++++++++++++++++--
taiga/projects/notifications/admin.py | 1 +
2 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 99757145..3349f360 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -22,13 +22,13 @@ from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from taiga.permissions import permissions
-from taiga.projects.milestones.admin import MilestoneInline
from taiga.projects.notifications.admin import NotifyPolicyInline
from taiga.projects.likes.admin import LikeInline
from taiga.users.admin import RoleInline
from . import models
+
class MembershipAdmin(admin.ModelAdmin):
list_display = ['project', 'role', 'user']
list_display_links = list_display
@@ -49,6 +49,7 @@ class MembershipAdmin(admin.ModelAdmin):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
class MembershipInline(admin.TabularInline):
model = models.Membership
extra = 0
@@ -77,10 +78,58 @@ class ProjectAdmin(admin.ModelAdmin):
list_filter = ("is_private", "blocked_code", "is_featured")
list_editable = ["is_featured", "blocked_code"]
search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"]
- inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline]
+ inlines = [RoleInline,
+ MembershipInline,
+ NotifyPolicyInline,
+ LikeInline]
- # NOTE: TextArrayField with a choices is broken in the admin panel.
- exclude = ("anon_permissions", "public_permissions")
+ fieldsets = (
+ (None, {
+ "fields": ("name",
+ "slug",
+ "is_featured",
+ "description",
+ "tags",
+ "logo",
+ ("created_date", "modified_date"))
+ }),
+ (_("Privacity"), {
+ "fields": (("owner", "blocked_code"),
+ "is_private",
+ ("anon_permissions", "public_permissions"),
+ "transfer_token")
+ }),
+ (_("Extra info"), {
+ "classes": ("collapse",),
+ "fields": ("creation_template",
+ ("is_looking_for_people", "looking_for_people_note"),
+ "tags_colors"),
+ }),
+ (_("Modules"), {
+ "classes": ("collapse",),
+ "fields": (("is_backlog_activated", "total_milestones", "total_story_points"),
+ "is_kanban_activated",
+ "is_issues_activated",
+ "is_wiki_activated",
+ ("videoconferences", "videoconferences_extra_data")),
+ }),
+ (_("Default values"), {
+ "classes": ("collapse",),
+ "fields": (("default_points", "default_us_status"),
+ "default_task_status",
+ ("default_issue_status", "default_priority", "default_severity", "default_issue_type")),
+ }),
+ (_("Activity"), {
+ "classes": ("collapse",),
+ "fields": (("total_activity", "total_activity_last_week",
+ "total_activity_last_month", "total_activity_last_year"),),
+ }),
+ (_("Fans"), {
+ "classes": ("collapse",),
+ "fields": (("total_fans", "total_fans_last_week",
+ "total_fans_last_month", "total_fans_last_year"),),
+ }),
+ )
def owner_url(self, obj):
if obj.owner:
diff --git a/taiga/projects/notifications/admin.py b/taiga/projects/notifications/admin.py
index b35dd206..07a70396 100644
--- a/taiga/projects/notifications/admin.py
+++ b/taiga/projects/notifications/admin.py
@@ -27,6 +27,7 @@ class WatchedInline(GenericTabularInline):
extra = 0
raw_id_fields = ["project", "user"]
+
class NotifyPolicyInline(TabularInline):
model = models.NotifyPolicy
extra = 0
From 4516dbca492c1ba436c0033f3ecb5ed68b1fb7b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 5 May 2016 13:27:40 +0200
Subject: [PATCH 008/261] Fix issue #4151: @mentions with dashes doesn't work
---
taiga/mdrender/extensions/mentions.py | 4 +--
tests/unit/test_mdrender.py | 41 +++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py
index 683250d2..d4620449 100644
--- a/taiga/mdrender/extensions/mentions.py
+++ b/taiga/mdrender/extensions/mentions.py
@@ -31,10 +31,10 @@ from markdown.util import etree, AtomicString
class MentionsExtension(Extension):
def extendMarkdown(self, md, md_globals):
- MENTION_RE = r'(@)([a-zA-Z0-9.-\._]+)'
+ MENTION_RE = r"(@)([\w.-]+)"
mentionsPattern = MentionsPattern(MENTION_RE)
mentionsPattern.md = md
- md.inlinePatterns.add('mentions', mentionsPattern, '_end')
+ md.inlinePatterns.add("mentions", mentionsPattern, "_end")
class MentionsPattern(Pattern):
diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py
index 0c3fb202..73ce4233 100644
--- a/tests/unit/test_mdrender.py
+++ b/tests/unit/test_mdrender.py
@@ -27,6 +27,8 @@ dummy_project = MagicMock()
dummy_project.id = 1
dummy_project.slug = "test"
+dummy_uuser = MagicMock()
+dummy_uuser.get_full_name.return_value = "Dummy User"
def test_proccessor_valid_emoji():
result = emojify.EmojifyPreprocessor().run(["**:smile:**"])
@@ -38,6 +40,45 @@ def test_proccessor_invalid_emoji():
assert result == ["**:notvalidemoji:**"]
+def test_mentions_valid_username():
+ with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock:
+ dummy_uuser = MagicMock()
+ dummy_uuser.get_full_name.return_value = "Hermione Granger"
+ get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser)
+
+ result = render(dummy_project, "text @hermione text")
+
+ get_user_model_mock.return_value.objects.get.assert_called_with(username="hermione")
+ assert result == ('text @hermione text
')
+
+
+def test_mentions_valid_username_with_points():
+ with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock:
+ dummy_uuser = MagicMock()
+ dummy_uuser.get_full_name.return_value = "Luna Lovegood"
+ get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser)
+
+ result = render(dummy_project, "text @luna.lovegood text")
+
+ get_user_model_mock.return_value.objects.get.assert_called_with(username="luna.lovegood")
+ assert result == ('text @luna.lovegood text
')
+
+
+def test_mentions_valid_username_with_dash():
+ with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock:
+ dummy_uuser = MagicMock()
+ dummy_uuser.get_full_name.return_value = "Ginny Weasley"
+ get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser)
+
+ result = render(dummy_project, "text @super-ginny text")
+
+ get_user_model_mock.return_value.objects.get.assert_called_with(username="super-ginny")
+ assert result == ('text @super-ginny text
')
+
+
def test_proccessor_valid_us_reference():
with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
instance = mock.return_value
From bdb5a433f899fb5c5ee30de40c2de5d064b3363c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 5 May 2016 13:32:06 +0200
Subject: [PATCH 009/261] Remove some unused vars
---
tests/unit/test_mdrender.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py
index 73ce4233..0ce3e5e9 100644
--- a/tests/unit/test_mdrender.py
+++ b/tests/unit/test_mdrender.py
@@ -27,8 +27,6 @@ dummy_project = MagicMock()
dummy_project.id = 1
dummy_project.slug = "test"
-dummy_uuser = MagicMock()
-dummy_uuser.get_full_name.return_value = "Dummy User"
def test_proccessor_valid_emoji():
result = emojify.EmojifyPreprocessor().run(["**:smile:**"])
From ec435f45bcb90389e569b059bcc7852c9c9eb5eb Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 5 May 2016 06:34:27 +0200
Subject: [PATCH 010/261] Import export projects with None as name for
ProjectRelatedField
---
taiga/export_import/serializers.py | 1 +
tests/integration/test_importer_api.py | 22 ++++++++++++++++++++--
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py
index 55b2031d..a8856f2f 100644
--- a/taiga/export_import/serializers.py
+++ b/taiga/export_import/serializers.py
@@ -160,6 +160,7 @@ class CommentField(serializers.WritableField):
class ProjectRelatedField(serializers.RelatedField):
read_only = False
+ null_values = (None, "")
def __init__(self, slug_field, *args, **kwargs):
self.slug_field = slug_field
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index b9e2d1c7..aff155ff 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -1107,6 +1107,26 @@ def test_services_store_project_from_dict_with_no_members_public_project_slots_a
assert "reaches your current limit of memberships for public" in str(excinfo.value)
+def test_services_store_project_from_dict_with_issue_priorities_names_as_None(client):
+ user = f.UserFactory.create()
+ data = {
+ "name": "Imported project",
+ "description": "Imported project",
+ "issue_types": [{"name": "Bug"}],
+ "issue_statuses": [{"name": "New"}],
+ "priorities": [{"name": "None", "order": 5, "color": "#CC0000"}],
+ "severities": [{"name": "Normal", "order": 5, "color": "#CC0000"}],
+ "issues": [{
+ "status": "New",
+ "priority": "None",
+ "severity": "Normal",
+ "type": "Bug",
+ "subject": "Test"}]}
+
+ project = services.store_project_from_dict(data, owner=user)
+ assert project.issues.first().priority.name == "None"
+
+
##################################################################
## tes api/v1/importer/load-dummp
##################################################################
@@ -1701,5 +1721,3 @@ def test_dump_import_duplicated_project(client):
assert response.status_code == 201
assert response.data["name"] == "Test import"
assert response.data["slug"] == "{}-test-import".format(user.username)
-
-
From eac0ee89cdb59b8880e7bc08ed14cb52821604d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 6 May 2016 10:38:04 +0200
Subject: [PATCH 011/261] Improve the logs when an importer process fail
---
taiga/export_import/services/store.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py
index fe34ff2a..dc521b26 100644
--- a/taiga/export_import/services/store.py
+++ b/taiga/export_import/services/store.py
@@ -618,7 +618,8 @@ def _create_project_object(data):
project_serialized = store_project(data)
if not project_serialized:
- raise err.TaigaImportError(_("error importing project data"), None)
+ errors = get_errors(clear=True)
+ raise err.TaigaImportError(_("error importing project data"), None, errors=errors)
return project_serialized.object if project_serialized else None
@@ -637,7 +638,7 @@ def _create_membership_for_project_owner(project):
def _populate_project_object(project, data):
def check_if_there_is_some_error(message=_("error importing project data"), project=None):
- errors = get_errors(clear=False)
+ errors = get_errors(clear=True)
if errors:
raise err.TaigaImportError(message, project, errors=errors)
@@ -710,8 +711,6 @@ def _populate_project_object(project, data):
def store_project_from_dict(data, owner=None):
- reset_errors()
-
# Validate
if owner:
_validate_if_owner_have_enought_space_to_this_project(owner, data)
From 9e897ca49999d11422463e3cf1cd0c3386cd1f4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 6 May 2016 16:42:03 +0200
Subject: [PATCH 012/261] Improve the import command
---
taiga/export_import/management/commands/load_dump.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index 08f7811b..fe6c4f9a 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -69,4 +69,4 @@ class Command(BaseCommand):
print("ERROR:", end=" ")
print(e.message)
- print(services.store.get_errors())
+ print(json.dumps(e.errors, indent=4))
From 576700afb6b7f092b07b71b4952b48d897b85013 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 9 May 2016 11:47:36 +0200
Subject: [PATCH 013/261] Fix error, import a bad model class
---
taiga/projects/tasks/services.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index e1f76f67..2736e317 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -89,7 +89,7 @@ def snapshot_tasks_in_bulk(bulk_data, user):
try:
task = models.Task.objects.get(pk=task_data['task_id'])
take_snapshot(task, user=user)
- except models.UserStory.DoesNotExist:
+ except models.Task.DoesNotExist:
pass
From 85acb93e22c369ada2132e4da983916b70e77337 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 11 May 2016 15:02:02 +0200
Subject: [PATCH 014/261] Improving performance for history api
---
taiga/projects/history/api.py | 2 +-
taiga/projects/history/models.py | 25 ++++++++++++++++++-------
taiga/projects/history/services.py | 13 ++++++++++++-
3 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py
index 9d958d0e..d10194ce 100644
--- a/taiga/projects/history/api.py
+++ b/taiga/projects/history/api.py
@@ -102,8 +102,8 @@ class HistoryViewSet(ReadOnlyListViewSet):
def retrieve(self, request, pk):
obj = self.get_object()
self.check_permissions(request, "retrieve", obj)
-
qs = services.get_history_queryset_by_model_instance(obj)
+ qs = services.prefetch_owners_in_history_queryset(qs)
return self.response_for_queryset(qs)
diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py
index d440900c..e947c6fe 100644
--- a/taiga/projects/history/models.py
+++ b/taiga/projects/history/models.py
@@ -78,6 +78,8 @@ class HistoryEntry(models.Model):
is_snapshot = models.BooleanField(default=False)
_importing = None
+ _owner = None
+ _prefetched_owner = False
@cached_property
def is_change(self):
@@ -91,14 +93,23 @@ class HistoryEntry(models.Model):
def is_delete(self):
return self.type == HistoryType.delete
- @cached_property
+ @property
def owner(self):
- pk = self.user["pk"]
- model = get_user_model()
- try:
- return model.objects.get(pk=pk)
- except model.DoesNotExist:
- return None
+ if not self._prefetched_owner:
+ pk = self.user["pk"]
+ model = get_user_model()
+ try:
+ owner = model.objects.get(pk=pk)
+ except model.DoesNotExist:
+ owner = None
+
+ self.prefetch_owner(owner)
+
+ return self._owner
+
+ def prefetch_owner(self, owner):
+ self._owner = owner
+ self._prefetched_owner = True
@cached_property
def values_diff(self):
diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py
index 399b486f..22839ec8 100644
--- a/taiga/projects/history/services.py
+++ b/taiga/projects/history/services.py
@@ -33,6 +33,7 @@ from functools import wraps
from functools import lru_cache
from django.conf import settings
+from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, InvalidPage
from django.apps import apps
@@ -331,7 +332,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
"is_hidden": is_hidden,
"is_snapshot": need_real_snapshot,
}
-
+
return entry_model.objects.create(**kwargs)
@@ -352,6 +353,16 @@ def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change
return qs.order_by("created_at")
+def prefetch_owners_in_history_queryset(qs):
+ user_ids = [u["pk"] for u in qs.values_list("user", flat=True)]
+ users = get_user_model().objects.filter(id__in=user_ids)
+ users_by_id = {u.id: u for u in users}
+ for history_entry in qs:
+ history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None))
+
+ return qs
+
+
# Freeze implementatitions
from .freeze_impl import project_freezer
from .freeze_impl import milestone_freezer
From 9474a1174f34d7d90e3046588cd26bd061e92dba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 10 May 2016 16:06:03 +0200
Subject: [PATCH 015/261] Made minor fixes over load_dump and dump_project
commands
---
.../management/commands/dump_project.py | 6 +--
.../management/commands/load_dump.py | 37 +++++++++++--------
2 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py
index d1248ad4..0b8938bc 100644
--- a/taiga/export_import/management/commands/dump_project.py
+++ b/taiga/export_import/management/commands/dump_project.py
@@ -24,7 +24,7 @@ import os
class Command(BaseCommand):
- help = "Export projects to json"
+ help = "Export projects to a json file"
def add_arguments(self, parser):
parser.add_argument("project_slugs",
@@ -56,7 +56,7 @@ class Command(BaseCommand):
raise CommandError("Project '{}' does not exist".format(project_slug))
dst_file = os.path.join(dst_dir, "{}.json".format(project_slug))
- with open(src_file, "w") as f:
+ with open(dst_file, "w") as f:
render_project(project, f)
- print("-> Generate dump of project '{}' in '{}'".format(project.name, src_file))
+ print("-> Generate dump of project '{}' in '{}'".format(project.name, dst_file))
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index fe6c4f9a..61209862 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -18,34 +18,39 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import signals
-from optparse import make_option
from taiga.base.utils import json
from taiga.export_import import services
from taiga.export_import import exceptions as err
-from taiga.export_import.renderers import ExportRenderer
from taiga.projects.models import Project
from taiga.users.models import User
class Command(BaseCommand):
- args = ' '
- help = 'Export a project to json'
- renderer_context = {"indent": 4}
- renderer = ExportRenderer()
- option_list = BaseCommand.option_list + (
- make_option('--overwrite',
- action='store_true',
- dest='overwrite',
- default=False,
- help='Delete project if exists'),
- )
+ help = 'Import a project from a json file'
+
+ def add_arguments(self, parser):
+ parser.add_argument("dump_file",
+ help="The path to a dump file (.json).")
+
+ parser.add_argument("owner_email",
+ help="The email of the new project owner.")
+
+ parser.add_argument("-o", '--overwrite',
+ action='store_true',
+ dest='overwrite',
+ default=False,
+ help='Overwrite the project if exists')
def handle(self, *args, **options):
- data = json.loads(open(args[0], 'r').read())
+ dump_file_path = options["dump_file"]
+ owner_email = options["owner_email"]
+ overwrite = options["overwrite"]
+
+ data = json.loads(open(dump_file_path, 'r').read())
try:
with transaction.atomic():
- if options["overwrite"]:
+ if overwrite:
receivers_back = signals.post_delete.receivers
signals.post_delete.receivers = []
try:
@@ -60,7 +65,7 @@ class Command(BaseCommand):
pass
signals.post_delete.receivers = receivers_back
- user = User.objects.get(email=args[1])
+ user = User.objects.get(email=owner_email)
services.store_project_from_dict(data, user)
except err.TaigaImportError as e:
if e.project:
From 4d1fb120b13287b791f625fa615c20c7f309ee27 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 11 May 2016 12:41:16 +0200
Subject: [PATCH 016/261] Improving performance when updating objects
---
taiga/permissions/service.py | 27 ++++++++++++++-----
taiga/projects/models.py | 30 +++++++++++++++++++--
taiga/projects/notifications/services.py | 23 +++++------------
taiga/projects/references/models.py | 3 ---
taiga/projects/services/tags_colors.py | 2 +-
taiga/timeline/service.py | 29 +++++++++++++++++++--
taiga/timeline/signals.py | 33 +++---------------------
taiga/webhooks/apps.py | 2 --
tests/integration/test_notifications.py | 21 +++++++++------
tests/unit/test_timeline.py | 8 +++---
10 files changed, 105 insertions(+), 73 deletions(-)
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index a56b3afc..32f79fdc 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -20,11 +20,18 @@ from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIO
from django.apps import apps
-def _get_user_project_membership(user, project):
+def _get_user_project_membership(user, project, cache="user"):
+ """
+ cache param determines how memberships are calculated trying to reuse the existing data
+ in cache
+ """
if user.is_anonymous():
return None
- return user.cached_membership_for_project(project)
+ if cache == "user":
+ return user.cached_membership_for_project(project)
+
+ return project.cached_memberships_for_user(user)
def _get_object_project(obj):
@@ -63,13 +70,17 @@ def is_project_admin(user, obj):
return False
-def user_has_perm(user, perm, obj=None):
+def user_has_perm(user, perm, obj=None, cache="user"):
+ """
+ cache param determines how memberships are calculated trying to reuse the existing data
+ in cache
+ """
project = _get_object_project(obj)
if not project:
return False
- return perm in get_user_project_permissions(user, project)
+ return perm in get_user_project_permissions(user, project, cache=cache)
def role_has_perm(role, perm):
@@ -82,8 +93,12 @@ def _get_membership_permissions(membership):
return []
-def get_user_project_permissions(user, project):
- membership = _get_user_project_membership(user, project)
+def get_user_project_permissions(user, project, cache="user"):
+ """
+ cache param determines how memberships are calculated trying to reuse the existing data
+ in cache
+ """
+ membership = _get_user_project_membership(user, project, cache=cache)
if user.is_superuser:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 730a62e3..cd38c35b 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -44,7 +44,6 @@ from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import (
- get_notify_policy,
set_notify_policy_level,
set_notify_policy_level_to_ignore,
create_notify_policy_if_not_exists)
@@ -345,6 +344,33 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
def cached_user_stories(self):
return list(self.user_stories.all())
+ @cached_property
+ def cached_notify_policies(self):
+ return {np.user.id: np for np in self.notify_policies.select_related("user", "project")}
+
+ def cached_notify_policy_for_user(self, user):
+ """
+ Get notification level for specified project and user.
+ """
+ policy = self.cached_notify_policies.get(user.id, None)
+ if policy is None:
+ model_cls = apps.get_model("notifications", "NotifyPolicy")
+ policy = model_cls.objects.create(
+ project=self,
+ user=user,
+ notify_level= NotifyLevel.involved)
+
+ del self.cached_notify_policies
+
+ return policy
+
+ @cached_property
+ def cached_memberships(self):
+ return {m.user.id: m for m in self.memberships.exclude(user__isnull=True).select_related("user", "project", "role")}
+
+ def cached_memberships_for_user(self, user):
+ return self.cached_memberships.get(user.id, None)
+
def get_roles(self):
return self.roles.all()
@@ -426,7 +452,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
set_notify_policy_level(notify_policy, notify_level)
def remove_watcher(self, user):
- notify_policy = get_notify_policy(self, user)
+ notify_policy = self.cached_notify_policy_for_user(user)
set_notify_policy_level_to_ignore(notify_policy)
def delete_related_content(self):
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 0a3cb8e7..22d518cb 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -78,16 +78,6 @@ def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.involved
raise exc.IntegrityError(_("Notify exists for specified user and project")) from e
-def get_notify_policy(project, user):
- """
- Get notification level for specified project and user.
- """
- model_cls = apps.get_model("notifications", "NotifyPolicy")
- instance, _ = model_cls.objects.get_or_create(project=project, user=user,
- defaults={"notify_level": NotifyLevel.involved})
- return instance
-
-
def analize_object_for_watchers(obj:object, comment:str, user:object):
"""
Generic implementation for analize model objects and
@@ -124,13 +114,13 @@ def _filter_by_permissions(obj, user):
WikiPage = apps.get_model("wiki", "WikiPage")
if isinstance(obj, UserStory):
- return user_has_perm(user, "view_us", obj)
+ return user_has_perm(user, "view_us", obj, cache="project")
elif isinstance(obj, Issue):
- return user_has_perm(user, "view_issues", obj)
+ return user_has_perm(user, "view_issues", obj, cache="project")
elif isinstance(obj, Task):
- return user_has_perm(user, "view_tasks", obj)
+ return user_has_perm(user, "view_tasks", obj, cache="project")
elif isinstance(obj, WikiPage):
- return user_has_perm(user, "view_wiki_pages", obj)
+ return user_has_perm(user, "view_wiki_pages", obj, cache="project")
return False
@@ -149,7 +139,7 @@ def get_users_to_notify(obj, *, discard_users=None) -> list:
project = obj.get_project()
def _check_level(project:object, user:object, levels:tuple) -> bool:
- policy = get_notify_policy(project, user)
+ policy = project.cached_notify_policy_for_user(user)
return policy.notify_level in levels
_can_notify_hard = partial(_check_level, project,
@@ -226,8 +216,7 @@ def send_notifications(obj, *, history):
# Get a complete list of notifiable users for current
# object and send the change notification to them.
notify_users = get_users_to_notify(obj, discard_users=[notification.owner])
- for notify_user in notify_users:
- notification.notify_users.add(notify_user)
+ notification.notify_users.add(*notify_users)
# If we are the min interval is 0 it just work in a synchronous and spamming way
if settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL == 0:
diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py
index bf3b072c..3bc63b1c 100644
--- a/taiga/projects/references/models.py
+++ b/taiga/projects/references/models.py
@@ -110,6 +110,3 @@ models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid
models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue")
models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask")
models.signals.post_delete.connect(delete_sequence, sender=Project, dispatch_uid="refprojdel")
-
-
-
diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py
index 9cebd044..90d44efa 100644
--- a/taiga/projects/services/tags_colors.py
+++ b/taiga/projects/services/tags_colors.py
@@ -54,7 +54,7 @@ def update_project_tags_colors_handler(instance):
new_color = _get_new_color(tag, settings.TAGS_PREDEFINED_COLORS,
exclude=used_colors)
instance.project.tags_colors.append([tag, new_color])
-
+
remove_unused_tags(instance.project)
if not isinstance(instance, Project):
diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py
index 65e27a89..12f7f29e 100644
--- a/taiga/timeline/service.py
+++ b/taiga/timeline/service.py
@@ -78,8 +78,7 @@ def _add_to_objects_timeline(objects, instance:object, event_type:str, created_d
_add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data)
-@app.task
-def push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data)
elif isinstance(objects, QuerySet) or isinstance(objects, list):
@@ -88,6 +87,32 @@ def push_to_timeline(objects, instance:object, event_type:str, created_datetime:
raise Exception("Invalid objects parameter")
+@app.task
+def push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}):
+ if project is not None:
+ # Actions related with a project
+
+ ## Project timeline
+ _push_to_timeline(project, obj, event_type, created_datetime,
+ namespace=build_project_namespace(project),
+ extra_data=extra_data)
+
+ project.refresh_totals()
+
+ if hasattr(obj, "get_related_people"):
+ related_people = obj.get_related_people()
+
+ _push_to_timeline(related_people, obj, event_type, created_datetime,
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
+ else:
+ # Actions not related with a project
+ ## - Me
+ _push_to_timeline(user, obj, event_type, created_datetime,
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
+
+
def get_timeline(obj, namespace=None):
assert isinstance(obj, Model), "obj must be a instance of Model"
from .models import Timeline
diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py
index 887688fc..eda08031 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -22,42 +22,17 @@ from django.utils.translation import ugettext as _
from taiga.projects.history import services as history_services
from taiga.projects.history.choices import HistoryType
-from taiga.timeline.service import (push_to_timeline,
+from taiga.timeline.service import (push_to_timelines,
build_user_namespace,
build_project_namespace,
extract_user_info)
-def _push_to_timeline(*args, **kwargs):
+def _push_to_timelines(*args, **kwargs):
if settings.CELERY_ENABLED:
- push_to_timeline.delay(*args, **kwargs)
+ push_to_timelines.delay(*args, **kwargs)
else:
- push_to_timeline(*args, **kwargs)
-
-
-def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}):
- if project is not None:
- # Actions related with a project
-
- ## Project timeline
- _push_to_timeline(project, obj, event_type, created_datetime,
- namespace=build_project_namespace(project),
- extra_data=extra_data)
-
- project.refresh_totals()
-
- if hasattr(obj, "get_related_people"):
- related_people = obj.get_related_people()
-
- _push_to_timeline(related_people, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
- else:
- # Actions not related with a project
- ## - Me
- _push_to_timeline(user, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
+ push_to_timelines(*args, **kwargs)
def _clean_description_fields(values_diff):
diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py
index a10cd1d2..9fff8e9b 100644
--- a/taiga/webhooks/apps.py
+++ b/taiga/webhooks/apps.py
@@ -27,8 +27,6 @@ def connect_webhooks_signals():
dispatch_uid="webhooks")
-
-
def disconnect_webhooks_signals():
signals.post_save.disconnect(sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="webhooks")
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index 85ea89fb..bb2cdfd8 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -60,7 +60,7 @@ def test_create_retrieve_notify_policy():
current_number = policy_model_cls.objects.all().count()
assert current_number == 0
- policy = services.get_notify_policy(project, project.owner)
+ policy = project.cached_notify_policy_for_user(project.owner)
current_number = policy_model_cls.objects.all().count()
assert current_number == 1
@@ -182,6 +182,7 @@ def test_users_to_notify():
policy_member1.notify_level = NotifyLevel.all
policy_member1.save()
+ del project.cached_notify_policies
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
@@ -190,6 +191,8 @@ def test_users_to_notify():
issue.add_watcher(member3.user)
policy_member3.notify_level = NotifyLevel.all
policy_member3.save()
+
+ del project.cached_notify_policies
users = services.get_users_to_notify(issue)
assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()}
@@ -199,12 +202,14 @@ def test_users_to_notify():
policy_member3.save()
issue.add_watcher(member3.user)
+ del project.cached_notify_policies
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
# Test with watchers without permissions
issue.add_watcher(member5.user)
+ del project.cached_notify_policies
users = services.get_users_to_notify(issue)
assert len(users) == 2
assert users == {member1.user, issue.get_owner()}
@@ -231,7 +236,7 @@ def test_watching_users_to_notify_on_issue_modification_1():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.all
@@ -250,7 +255,7 @@ def test_watching_users_to_notify_on_issue_modification_2():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.involved
@@ -269,7 +274,7 @@ def test_watching_users_to_notify_on_issue_modification_3():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
issue.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.none
@@ -289,7 +294,7 @@ def test_watching_users_to_notify_on_issue_modification_4():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.none
@@ -309,7 +314,7 @@ def test_watching_users_to_notify_on_issue_modification_5():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.all
@@ -329,7 +334,7 @@ def test_watching_users_to_notify_on_issue_modification_6():
issue = f.IssueFactory.create(project=project)
watching_user = f.UserFactory()
project.add_watcher(watching_user)
- watching_user_policy = services.get_notify_policy(project, watching_user)
+ watching_user_policy = project.cached_notify_policy_for_user(watching_user)
issue.description = "test1"
issue.save()
watching_user_policy.notify_level = NotifyLevel.involved
@@ -902,7 +907,7 @@ def test_watchers_assignation_for_us(client):
def test_retrieve_notify_policies_by_anonymous_user(client):
project = f.ProjectFactory.create()
- policy = services.get_notify_policy(project, project.owner)
+ policy = project.cached_notify_policy_for_user(project.owner)
url = reverse("notifications-detail", args=[policy.pk])
response = client.get(url, content_type="application/json")
diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py
index e34906d6..18c679e5 100644
--- a/tests/unit/test_timeline.py
+++ b/tests/unit/test_timeline.py
@@ -26,12 +26,14 @@ from taiga.projects.models import Project
import pytest
+pytestmark = pytest.mark.django_db
def test_push_to_timeline_many_objects():
with patch("taiga.timeline.service._add_to_object_timeline") as mock:
users = [get_user_model(), get_user_model(), get_user_model()]
+ owner = get_user_model()
project = Project()
- service.push_to_timeline(users, project, "test", project.created_date)
+ service._push_to_timeline(users, project, "test", project.created_date)
assert mock.call_count == 3
assert mock.mock_calls == [
call(users[0], project, "test", project.created_date, "default", {}),
@@ -39,7 +41,7 @@ def test_push_to_timeline_many_objects():
call(users[2], project, "test", project.created_date, "default", {}),
]
with pytest.raises(Exception):
- service.push_to_timeline(None, project, "test")
+ service._push_to_timeline(None, project, "test")
def test_add_to_objects_timeline():
@@ -54,7 +56,7 @@ def test_add_to_objects_timeline():
call(users[2], project, "test", project.created_date, "default", {}),
]
with pytest.raises(Exception):
- service.push_to_timeline(None, project, "test")
+ service._push_to_timeline(None, project, "test")
def test_get_impl_key_from_model():
From a7dfec1fc9460ecf36cb792e2e3e33544156d8d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 24 May 2016 14:17:52 +0200
Subject: [PATCH 017/261] Fix support pages url
---
settings/sr.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/settings/sr.py b/settings/sr.py
index 4786e512..bc58e8e0 100644
--- a/settings/sr.py
+++ b/settings/sr.py
@@ -23,7 +23,7 @@ SR = {
"github_url": "https://github.com/taigaio",
},
"support": {
- "url": "https://taiga.io/support",
+ "url": "https://tree.taiga.io/support",
"email": "support@taiga.io",
"mailing_list": "https://groups.google.com/forum/#!forum/taigaio",
}
From e295bd95d80632311645a4450241b27ef63e7b3c Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 24 May 2016 14:56:50 +0200
Subject: [PATCH 018/261] Ugly hack to temporary disable thumbnail generation
for tiff files
---
taiga/base/utils/thumbnails.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/taiga/base/utils/thumbnails.py b/taiga/base/utils/thumbnails.py
index 2c2ffca0..1337ad84 100644
--- a/taiga/base/utils/thumbnails.py
+++ b/taiga/base/utils/thumbnails.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import os
+
+from django.db.models.fields.files import FieldFile
+
from taiga.base.utils.urls import get_absolute_url
from easy_thumbnails.files import get_thumbnailer
@@ -22,6 +26,15 @@ from easy_thumbnails.exceptions import InvalidImageFormatError
def get_thumbnail_url(file_obj, thumbnailer_size):
+ # Ugly hack to temporary ignore tiff files
+ relative_name = file_obj
+ if isinstance(file_obj, FieldFile):
+ relative_name = file_obj.name
+
+ source_extension = os.path.splitext(relative_name)[1][1:]
+ if source_extension == "tiff":
+ return None
+
try:
path_url = get_thumbnailer(file_obj)[thumbnailer_size].url
thumb_url = get_absolute_url(path_url)
From 827d1b61328a9a0270cce182769170f2424dca22 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 23 May 2016 13:22:45 +0200
Subject: [PATCH 019/261] Fixing timeline permissions for admin and superusers
---
taiga/timeline/service.py | 14 +++++++++---
tests/integration/test_timeline.py | 34 ++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py
index 12f7f29e..11517542 100644
--- a/taiga/timeline/service.py
+++ b/taiga/timeline/service.py
@@ -128,6 +128,10 @@ def get_timeline(obj, namespace=None):
def filter_timeline_for_user(timeline, user):
+ # Superusers can see everything
+ if user.is_superuser:
+ return timeline
+
# Filtering entities from public projects or entities without project
tl_filter = Q(project__is_private=False) | Q(project=None)
@@ -156,9 +160,13 @@ def filter_timeline_for_user(timeline, user):
# Filtering private projects where user is member
if not user.is_anonymous():
for membership in user.cached_memberships:
- data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions]))
- data_content_types.append(membership_content_type)
- tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types)
+ # Admin roles can see everything in a project
+ if membership.is_admin:
+ tl_filter |= Q(project=membership.project)
+ else:
+ data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions]))
+ data_content_types.append(membership_content_type)
+ tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types)
timeline = timeline.filter(tl_filter)
return timeline
diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py
index 81912fdb..8c8e0182 100644
--- a/tests/integration/test_timeline.py
+++ b/tests/integration/test_timeline.py
@@ -130,6 +130,40 @@ def test_filter_timeline_private_project_member_permissions():
assert timeline.count() == 3
+def test_filter_timeline_private_project_member_admin():
+ Timeline.objects.all().delete()
+ user1 = factories.UserFactory()
+ user2 = factories.UserFactory()
+ project = factories.ProjectFactory.create(is_private=True)
+ membership = factories.MembershipFactory.create(user=user2, project=project, is_admin=True)
+ task1= factories.TaskFactory()
+ task2= factories.TaskFactory.create(project=project)
+
+ service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
+ service._add_to_object_timeline(user1, task1, "test", task1.created_date)
+ service._add_to_object_timeline(user1, task2, "test", task2.created_date)
+ timeline = Timeline.objects.exclude(event_type="users.user.create")
+ timeline = service.filter_timeline_for_user(timeline, user2)
+ assert timeline.count() == 3
+
+
+def test_filter_timeline_private_project_member_superuser():
+ Timeline.objects.all().delete()
+ user1 = factories.UserFactory()
+ user2 = factories.UserFactory(is_superuser=True)
+ project = factories.ProjectFactory.create(is_private=True)
+
+ task1= factories.TaskFactory()
+ task2= factories.TaskFactory.create(project=project)
+
+ service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
+ service._add_to_object_timeline(user1, task1, "test", task1.created_date)
+ service._add_to_object_timeline(user1, task2, "test", task2.created_date)
+ timeline = Timeline.objects.exclude(event_type="users.user.create")
+ timeline = service.filter_timeline_for_user(timeline, user2)
+ assert timeline.count() == 2
+
+
def test_create_project_timeline():
project = factories.ProjectFactory.create(name="test project timeline")
history_services.take_snapshot(project, user=project.owner)
From 81b41529d2b6864ab69fe25c7fb46cab51d4ee94 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 9 May 2016 11:50:49 +0200
Subject: [PATCH 020/261] US #2929: Ability to edit commit and see their
history
---
CHANGELOG.md | 4 +
taiga/projects/history/api.py | 84 +-
.../migrations/0009_auto_20160512_1110.py | 26 +
taiga/projects/history/models.py | 18 +
taiga/projects/history/permissions.py | 27 +-
taiga/projects/history/serializers.py | 2 +
taiga/projects/history/services.py | 14 +
taiga/timeline/signals.py | 5 +-
.../test_history_resources.py | 897 +++++++++++++++++-
tests/integration/test_history.py | 83 ++
10 files changed, 1102 insertions(+), 58 deletions(-)
create mode 100644 taiga/projects/history/migrations/0009_auto_20160512_1110.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b5f288a..eca3114a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog #
+## 2.2.0 ??? (unreleased)
+### Features
+- [API] edit comment endpoint: comment owners and project admins can edit existing comments
+
## 2.1.0 Ursus Americanus (2016-05-03)
diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py
index d10194ce..3f1ca240 100644
--- a/taiga/projects/history/api.py
+++ b/taiga/projects/history/api.py
@@ -23,6 +23,7 @@ from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import ReadOnlyListViewSet
from taiga.base.api.utils import get_object_or_404
+from taiga.mdrender.service import render as mdrender
from . import permissions
from . import serializers
@@ -56,42 +57,93 @@ class HistoryViewSet(ReadOnlyListViewSet):
return response.Ok(serializer.data)
+ @detail_route(methods=['get'])
+ def comment_versions(self, request, pk):
+ obj = self.get_object()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+
+ self.check_permissions(request, 'comment_versions', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ history_entry.attach_user_info_to_comment_versions()
+ return response.Ok(history_entry.comment_versions)
+
+ @detail_route(methods=['post'])
+ def edit_comment(self, request, pk):
+ obj = self.get_object()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+ obj = services.get_instance_from_key(history_entry.key)
+ comment = request.DATA.get("comment", None)
+
+ self.check_permissions(request, 'edit_comment', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ if comment is None:
+ return response.BadRequest({"error": _("comment is required")})
+
+ if history_entry.delete_comment_date or history_entry.delete_comment_user:
+ return response.BadRequest({"error": _("deleted comments can't be edited")})
+
+ # comment_versions can be None if there are no historic versions of the comment
+ comment_versions = history_entry.comment_versions or []
+ comment_versions.append({
+ "date": history_entry.created_at,
+ "comment": history_entry.comment,
+ "comment_html": history_entry.comment_html,
+ "user": {
+ "id": request.user.pk,
+ }
+ })
+
+ history_entry.edit_comment_date = timezone.now()
+ history_entry.comment = comment
+ history_entry.comment_html = mdrender(obj.project, comment)
+ history_entry.comment_versions = comment_versions
+ history_entry.save()
+ return response.Ok()
+
@detail_route(methods=['post'])
def delete_comment(self, request, pk):
obj = self.get_object()
- comment_id = request.QUERY_PARAMS.get('id', None)
- comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
- self.check_permissions(request, 'delete_comment', comment)
+ self.check_permissions(request, 'delete_comment', history_entry)
- if comment is None:
+ if history_entry is None:
return response.NotFound()
- if comment.delete_comment_date or comment.delete_comment_user:
+ if history_entry.delete_comment_date or history_entry.delete_comment_user:
return response.BadRequest({"error": _("Comment already deleted")})
- comment.delete_comment_date = timezone.now()
- comment.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()}
- comment.save()
+ history_entry.delete_comment_date = timezone.now()
+ history_entry.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()}
+ history_entry.save()
return response.Ok()
@detail_route(methods=['post'])
def undelete_comment(self, request, pk):
obj = self.get_object()
- comment_id = request.QUERY_PARAMS.get('id', None)
- comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
- self.check_permissions(request, 'undelete_comment', comment)
+ self.check_permissions(request, 'undelete_comment', history_entry)
- if comment is None:
+ if history_entry is None:
return response.NotFound()
- if not comment.delete_comment_date and not comment.delete_comment_user:
+ if not history_entry.delete_comment_date and not history_entry.delete_comment_user:
return response.BadRequest({"error": _("Comment not deleted")})
- comment.delete_comment_date = None
- comment.delete_comment_user = None
- comment.save()
+ history_entry.delete_comment_date = None
+ history_entry.delete_comment_user = None
+ history_entry.save()
return response.Ok()
# Just for restframework! Because it raises
diff --git a/taiga/projects/history/migrations/0009_auto_20160512_1110.py b/taiga/projects/history/migrations/0009_auto_20160512_1110.py
new file mode 100644
index 00000000..0cf39023
--- /dev/null
+++ b/taiga/projects/history/migrations/0009_auto_20160512_1110.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-12 11:10
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django_pgjson.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('history', '0008_auto_20150508_1028'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historyentry',
+ name='comment_versions',
+ field=django_pgjson.fields.JsonField(blank=True, default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name='historyentry',
+ name='edit_comment_date',
+ field=models.DateTimeField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py
index e947c6fe..39012f60 100644
--- a/taiga/projects/history/models.py
+++ b/taiga/projects/history/models.py
@@ -67,6 +67,10 @@ class HistoryEntry(models.Model):
delete_comment_date = models.DateTimeField(null=True, blank=True, default=None)
delete_comment_user = JsonField(null=True, blank=True, default=None)
+ # Historic version of comments
+ comment_versions = JsonField(null=True, blank=True, default=None)
+ edit_comment_date = models.DateTimeField(null=True, blank=True, default=None)
+
# Flag for mark some history entries as
# hidden. Hidden history entries are important
# for save but not important to preview.
@@ -111,6 +115,20 @@ class HistoryEntry(models.Model):
self._owner = owner
self._prefetched_owner = True
+ def attach_user_info_to_comment_versions(self):
+ if not self.comment_versions:
+ return
+
+ from taiga.users.serializers import UserSerializer
+
+ user_ids = [v["user"]["id"] for v in self.comment_versions if "user" in v and "id" in v["user"]]
+ users_by_id = {u.id: u for u in get_user_model().objects.filter(id__in=user_ids)}
+
+ for version in self.comment_versions:
+ user = users_by_id.get(version["user"]["id"], None)
+ if user:
+ version["user"] = UserSerializer(user).data
+
@cached_property
def values_diff(self):
result = {}
diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py
index 015ac22c..fdce68cd 100644
--- a/taiga/projects/history/permissions.py
+++ b/taiga/projects/history/permissions.py
@@ -33,32 +33,41 @@ class IsCommentOwner(PermissionComponent):
return obj.user and obj.user.get("pk", "not-pk") == request.user.pk
-class IsCommentProjectOwner(PermissionComponent):
+class IsCommentProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
model = get_model_from_key(obj.key)
pk = get_pk_from_key(obj.key)
project = model.objects.get(pk=pk)
return is_project_admin(request.user, project)
+
class UserStoryHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class TaskHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class IssueHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class WikiHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py
index 72b3c763..f231f29c 100644
--- a/taiga/projects/history/serializers.py
+++ b/taiga/projects/history/serializers.py
@@ -33,9 +33,11 @@ class HistoryEntrySerializer(serializers.ModelSerializer):
values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
user = serializers.SerializerMethodField("get_user")
delete_comment_user = JsonField()
+ comment_versions = JsonField()
class Meta:
model = models.HistoryEntry
+ exclude = ("comment_versions",)
def get_user(self, entry):
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}
diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py
index 22839ec8..61004471 100644
--- a/taiga/projects/history/services.py
+++ b/taiga/projects/history/services.py
@@ -91,6 +91,20 @@ def get_pk_from_key(key:str) -> object:
return pk
+def get_instance_from_key(key:str) -> object:
+ """
+ Get instance from key
+ """
+ model = get_model_from_key(key)
+ pk = get_pk_from_key(key)
+ try:
+ obj = model.objects.get(pk=pk)
+ return obj
+ except model.DoesNotExist:
+ # Catch simultaneous DELETE request
+ return None
+
+
def register_values_implementation(typename:str, fn=None):
"""
Register values implementation for specified typename.
diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py
index eda08031..c0f1dffa 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -44,7 +44,6 @@ def _clean_description_fields(values_diff):
def on_new_history_entry(sender, instance, created, **kwargs):
-
if instance._importing:
return
@@ -81,6 +80,10 @@ def on_new_history_entry(sender, instance, created, **kwargs):
if instance.delete_comment_date:
extra_data["comment_deleted"] = True
+ # Detect edited comment
+ if instance.comment_versions is not None and len(instance.comment_versions)>0:
+ extra_data["comment_edited"] = True
+
created_datetime = instance.created_at
_push_to_timelines(project, user, obj, event_type, created_datetime, extra_data=extra_data)
diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py
index b0991080..be17510c 100644
--- a/tests/integration/resources_permissions/test_history_resources.py
+++ b/tests/integration/resources_permissions/test_history_resources.py
@@ -1,6 +1,11 @@
from django.core.urlresolvers import reverse
+from django.utils import timezone
+from taiga.base.utils import json
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.projects.history.models import HistoryEntry
+from taiga.projects.history.choices import HistoryType
+from taiga.projects.history.services import make_key_from_model_object
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -21,11 +26,11 @@ def teardown_module(module):
def data():
m = type("Models", (object,), {})
- m.registered_user = f.UserFactory.create()
- m.project_member_with_perms = f.UserFactory.create()
- m.project_member_without_perms = f.UserFactory.create()
- m.project_owner = f.UserFactory.create()
- m.other_user = f.UserFactory.create()
+ m.registered_user = f.UserFactory.create(full_name="registered_user")
+ m.project_member_with_perms = f.UserFactory.create(full_name="project_member_with_perms")
+ m.project_member_without_perms = f.UserFactory.create(full_name="project_member_without_perms")
+ m.project_owner = f.UserFactory.create(full_name="project_owner")
+ m.other_user = f.UserFactory.create(full_name="other_user")
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
@@ -76,39 +81,33 @@ def data():
return m
+#########################################################
+## User stories
+#########################################################
+
+
@pytest.fixture
def data_us(data):
m = type("Models", (object,), {})
m.public_user_story = f.UserStoryFactory(project=data.public_project, ref=1)
+ m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing public",
+ key=make_key_from_model_object(m.public_user_story),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+
m.private_user_story1 = f.UserStoryFactory(project=data.private_project1, ref=5)
+ m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 1",
+ key=make_key_from_model_object(m.private_user_story1),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9)
- return m
-
-
-@pytest.fixture
-def data_task(data):
- m = type("Models", (object,), {})
- m.public_task = f.TaskFactory(project=data.public_project, ref=2)
- m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6)
- m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10)
- return m
-
-
-@pytest.fixture
-def data_issue(data):
- m = type("Models", (object,), {})
- m.public_issue = f.IssueFactory(project=data.public_project, ref=3)
- m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7)
- m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11)
- return m
-
-
-@pytest.fixture
-def data_wiki(data):
- m = type("Models", (object,), {})
- m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4)
- m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8)
- m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12)
+ m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 2",
+ key=make_key_from_model_object(m.private_user_story2),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
return m
@@ -133,6 +132,222 @@ def test_user_story_history_retrieve(client, data, data_us):
assert results == [401, 403, 403, 200, 200]
+def test_user_story_action_edit_comment(client, data, data_us):
+ public_url = "{}?id={}".format(
+ reverse('userstory-history-edit-comment', kwargs={"pk": data_us.public_user_story.pk}),
+ data_us.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story1.pk}),
+ data_us.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story2.pk}),
+ data_us.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ data = json.dumps({"comment": "testing update comment"})
+
+ results = helper_test_http_method(client, 'post', public_url, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_user_story_action_delete_comment(client, data, data_us):
+ public_url = "{}?id={}".format(
+ reverse('userstory-history-delete-comment', kwargs={"pk": data_us.public_user_story.pk}),
+ data_us.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story1.pk}),
+ data_us.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story2.pk}),
+ data_us.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_us.public_history_entry.delete_comment_date = None
+ data_us.public_history_entry.delete_comment_user = None
+ data_us.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_us.private_history_entry1.delete_comment_date = None
+ data_us.private_history_entry1.delete_comment_user = None
+ data_us.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_us.private_history_entry2.delete_comment_date = None
+ data_us.private_history_entry2.delete_comment_user = None
+ data_us.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_user_story_action_undelete_comment(client, data, data_us):
+ public_url = "{}?id={}".format(
+ reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.public_user_story.pk}),
+ data_us.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story1.pk}),
+ data_us.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story2.pk}),
+ data_us.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_us.public_history_entry.delete_comment_date = timezone.now()
+ data_us.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_us.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_us.private_history_entry1.delete_comment_date = timezone.now()
+ data_us.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_us.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_us.private_history_entry2.delete_comment_date = timezone.now()
+ data_us.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_us.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_user_story_action_comment_versions(client, data, data_us):
+ public_url = "{}?id={}".format(
+ reverse('userstory-history-comment-versions', kwargs={"pk": data_us.public_user_story.pk}),
+ data_us.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story1.pk}),
+ data_us.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story2.pk}),
+ data_us.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner,
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+#########################################################
+## Tasks
+#########################################################
+
+
+@pytest.fixture
+def data_task(data):
+ m = type("Models", (object,), {})
+ m.public_task = f.TaskFactory(project=data.public_project, ref=2)
+ m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing public",
+ key=make_key_from_model_object(m.public_task),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+
+ m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6)
+ m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 1",
+ key=make_key_from_model_object(m.private_task1),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10)
+ m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 2",
+ key=make_key_from_model_object(m.private_task2),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ return m
+
+
def test_task_history_retrieve(client, data, data_task):
public_url = reverse('task-history-detail', kwargs={"pk": data_task.public_task.pk})
private_url1 = reverse('task-history-detail', kwargs={"pk": data_task.private_task1.pk})
@@ -154,6 +369,222 @@ def test_task_history_retrieve(client, data, data_task):
assert results == [401, 403, 403, 200, 200]
+def test_task_action_edit_comment(client, data, data_task):
+ public_url = "{}?id={}".format(
+ reverse('task-history-edit-comment', kwargs={"pk": data_task.public_task.pk}),
+ data_task.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task1.pk}),
+ data_task.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task2.pk}),
+ data_task.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ data = json.dumps({"comment": "testing update comment"})
+
+ results = helper_test_http_method(client, 'post', public_url, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_task_action_delete_comment(client, data, data_task):
+ public_url = "{}?id={}".format(
+ reverse('task-history-delete-comment', kwargs={"pk": data_task.public_task.pk}),
+ data_task.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task1.pk}),
+ data_task.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task2.pk}),
+ data_task.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_task.public_history_entry.delete_comment_date = None
+ data_task.public_history_entry.delete_comment_user = None
+ data_task.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_task.private_history_entry1.delete_comment_date = None
+ data_task.private_history_entry1.delete_comment_user = None
+ data_task.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_task.private_history_entry2.delete_comment_date = None
+ data_task.private_history_entry2.delete_comment_user = None
+ data_task.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_task_action_undelete_comment(client, data, data_task):
+ public_url = "{}?id={}".format(
+ reverse('task-history-undelete-comment', kwargs={"pk": data_task.public_task.pk}),
+ data_task.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task1.pk}),
+ data_task.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task2.pk}),
+ data_task.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_task.public_history_entry.delete_comment_date = timezone.now()
+ data_task.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_task.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_task.private_history_entry1.delete_comment_date = timezone.now()
+ data_task.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_task.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_task.private_history_entry2.delete_comment_date = timezone.now()
+ data_task.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_task.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_task_action_comment_versions(client, data, data_task):
+ public_url = "{}?id={}".format(
+ reverse('task-history-comment-versions', kwargs={"pk": data_task.public_task.pk}),
+ data_task.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task1.pk}),
+ data_task.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task2.pk}),
+ data_task.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner,
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+#########################################################
+## Issues
+#########################################################
+
+
+@pytest.fixture
+def data_issue(data):
+ m = type("Models", (object,), {})
+ m.public_issue = f.IssueFactory(project=data.public_project, ref=3)
+ m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing public",
+ key=make_key_from_model_object(m.public_issue),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+
+ m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7)
+ m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 1",
+ key=make_key_from_model_object(m.private_issue1),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11)
+ m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 2",
+ key=make_key_from_model_object(m.private_issue2),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ return m
+
+
def test_issue_history_retrieve(client, data, data_issue):
public_url = reverse('issue-history-detail', kwargs={"pk": data_issue.public_issue.pk})
private_url1 = reverse('issue-history-detail', kwargs={"pk": data_issue.private_issue1.pk})
@@ -175,6 +606,222 @@ def test_issue_history_retrieve(client, data, data_issue):
assert results == [401, 403, 403, 200, 200]
+def test_issue_action_edit_comment(client, data, data_issue):
+ public_url = "{}?id={}".format(
+ reverse('issue-history-edit-comment', kwargs={"pk": data_issue.public_issue.pk}),
+ data_issue.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue1.pk}),
+ data_issue.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue2.pk}),
+ data_issue.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ data = json.dumps({"comment": "testing update comment"})
+
+ results = helper_test_http_method(client, 'post', public_url, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_issue_action_delete_comment(client, data, data_issue):
+ public_url = "{}?id={}".format(
+ reverse('issue-history-delete-comment', kwargs={"pk": data_issue.public_issue.pk}),
+ data_issue.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue1.pk}),
+ data_issue.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue2.pk}),
+ data_issue.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_issue.public_history_entry.delete_comment_date = None
+ data_issue.public_history_entry.delete_comment_user = None
+ data_issue.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_issue.private_history_entry1.delete_comment_date = None
+ data_issue.private_history_entry1.delete_comment_user = None
+ data_issue.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_issue.private_history_entry2.delete_comment_date = None
+ data_issue.private_history_entry2.delete_comment_user = None
+ data_issue.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_issue_action_undelete_comment(client, data, data_issue):
+ public_url = "{}?id={}".format(
+ reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.public_issue.pk}),
+ data_issue.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue1.pk}),
+ data_issue.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue2.pk}),
+ data_issue.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_issue.public_history_entry.delete_comment_date = timezone.now()
+ data_issue.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_issue.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_issue.private_history_entry1.delete_comment_date = timezone.now()
+ data_issue.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_issue.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_issue.private_history_entry2.delete_comment_date = timezone.now()
+ data_issue.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_issue.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_issue_action_comment_versions(client, data, data_issue):
+ public_url = "{}?id={}".format(
+ reverse('issue-history-comment-versions', kwargs={"pk": data_issue.public_issue.pk}),
+ data_issue.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue1.pk}),
+ data_issue.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue2.pk}),
+ data_issue.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner,
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+#########################################################
+## Wiki pages
+#########################################################
+
+
+@pytest.fixture
+def data_wiki(data):
+ m = type("Models", (object,), {})
+ m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4)
+ m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing public",
+ key=make_key_from_model_object(m.public_wiki),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+
+ m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8)
+ m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 1",
+ key=make_key_from_model_object(m.private_wiki1),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12)
+ m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing 2",
+ key=make_key_from_model_object(m.private_wiki2),
+ diff={},
+ user={"pk": data.project_member_with_perms.pk})
+ return m
+
+
def test_wiki_history_retrieve(client, data, data_wiki):
public_url = reverse('wiki-history-detail', kwargs={"pk": data_wiki.public_wiki.pk})
private_url1 = reverse('wiki-history-detail', kwargs={"pk": data_wiki.private_wiki1.pk})
@@ -194,3 +841,189 @@ def test_wiki_history_retrieve(client, data, data_wiki):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_wiki_action_edit_comment(client, data, data_wiki):
+ public_url = "{}?id={}".format(
+ reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.public_wiki.pk}),
+ data_wiki.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki1.pk}),
+ data_wiki.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki2.pk}),
+ data_wiki.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ data = json.dumps({"comment": "testing update comment"})
+
+ results = helper_test_http_method(client, 'post', public_url, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url1, data, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'post', private_url2, data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_wiki_action_delete_comment(client, data, data_wiki):
+ public_url = "{}?id={}".format(
+ reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.public_wiki.pk}),
+ data_wiki.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}),
+ data_wiki.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}),
+ data_wiki.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_wiki.public_history_entry.delete_comment_date = None
+ data_wiki.public_history_entry.delete_comment_user = None
+ data_wiki.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_wiki.private_history_entry1.delete_comment_date = None
+ data_wiki.private_history_entry1.delete_comment_user = None
+ data_wiki.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_wiki.private_history_entry2.delete_comment_date = None
+ data_wiki.private_history_entry2.delete_comment_user = None
+ data_wiki.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_wiki_action_undelete_comment(client, data, data_wiki):
+ public_url = "{}?id={}".format(
+ reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.public_wiki.pk}),
+ data_wiki.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}),
+ data_wiki.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}),
+ data_wiki.private_history_entry2.id
+ )
+
+ users_and_statuses = [
+ (None, 401),
+ (data.registered_user, 403),
+ (data.project_member_without_perms, 403),
+ (data.project_member_with_perms, 200),
+ (data.project_owner, 200),
+ ]
+
+ for user, status_code in users_and_statuses:
+ data_wiki.public_history_entry.delete_comment_date = timezone.now()
+ data_wiki.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_wiki.public_history_entry.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(public_url)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_wiki.private_history_entry1.delete_comment_date = timezone.now()
+ data_wiki.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_wiki.private_history_entry1.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url1)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+ for user, status_code in users_and_statuses:
+ data_wiki.private_history_entry2.delete_comment_date = timezone.now()
+ data_wiki.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk}
+ data_wiki.private_history_entry2.save()
+
+ if user:
+ client.login(user)
+ else:
+ client.logout()
+ response = client.json.post(private_url2)
+ error_mesage = "{} != {} for {}".format(response.status_code, status_code, user)
+ assert response.status_code == status_code, error_mesage
+
+
+def test_wiki_action_comment_versions(client, data, data_wiki):
+ public_url = "{}?id={}".format(
+ reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.public_wiki.pk}),
+ data_wiki.public_history_entry.id
+ )
+ private_url1 = "{}?id={}".format(
+ reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki1.pk}),
+ data_wiki.private_history_entry1.id
+ )
+ private_url2 = "{}?id={}".format(
+ reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki2.pk}),
+ data_wiki.private_history_entry2.id
+ )
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner,
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py
index 01469c0a..5d936c0d 100644
--- a/tests/integration/test_history.py
+++ b/tests/integration/test_history.py
@@ -17,6 +17,8 @@
# along with this program. If not, see .
import pytest
+import datetime
+
from unittest.mock import patch
from django.core.urlresolvers import reverse
@@ -235,3 +237,84 @@ def test_delete_comment_by_project_owner(client):
url = "%s?id=%s" % (url, history_entry.id)
response = client.post(url, content_type="application/json")
assert 200 == response.status_code, response.status_code
+
+
+def test_edit_comment(client):
+ project = f.create_project()
+ us = f.create_userstory(project=project)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ key = make_key_from_model_object(us)
+ history_entry = f.HistoryEntryFactory.create(type=HistoryType.change,
+ comment="testing",
+ key=key,
+ diff={},
+ user={"pk": project.owner.id})
+
+ history_entry_created_at = history_entry.created_at
+ assert history_entry.comment_versions == None
+ assert history_entry.edit_comment_date == None
+
+ client.login(project.owner)
+ url = reverse("userstory-history-edit-comment", args=(us.id,))
+ url = "%s?id=%s" % (url, history_entry.id)
+
+ data = json.dumps({"comment": "testing update comment"})
+ response = client.post(url, data, content_type="application/json")
+ assert 200 == response.status_code, response.status_code
+
+
+ history_entry = HistoryEntry.objects.get(id=history_entry.id)
+ assert len(history_entry.comment_versions) == 1
+ assert history_entry.comment == "testing update comment"
+ assert history_entry.comment_versions[0]["comment"] == "testing"
+ assert history_entry.edit_comment_date != None
+ assert history_entry.comment_versions[0]["user"]["id"] == project.owner.id
+
+
+def test_get_comment_versions(client):
+ project = f.create_project()
+ us = f.create_userstory(project=project)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ key = make_key_from_model_object(us)
+ history_entry = f.HistoryEntryFactory.create(
+ type=HistoryType.change,
+ comment="testing",
+ key=key,
+ diff={},
+ user={"pk": project.owner.id},
+ edit_comment_date=datetime.datetime.now(),
+ comment_versions = [{
+ "comment_html": "test
",
+ "date": "2016-05-09T09:34:27.221Z",
+ "comment": "test",
+ "user": {
+ "id": project.owner.id,
+ }}])
+
+ client.login(project.owner)
+ url = reverse("userstory-history-comment-versions", args=(us.id,))
+ url = "%s?id=%s" % (url, history_entry.id)
+
+ response = client.get(url, content_type="application/json")
+ assert 200 == response.status_code, response.status_code
+ assert response.data[0]["user"]["username"] == project.owner.username
+
+
+def test_get_comment_versions_from_history_entry_without_comment(client):
+ project = f.create_project()
+ us = f.create_userstory(project=project)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ key = make_key_from_model_object(us)
+ history_entry = f.HistoryEntryFactory.create(
+ type=HistoryType.change,
+ key=key,
+ diff={},
+ user={"pk": project.owner.id})
+
+ client.login(project.owner)
+ url = reverse("userstory-history-comment-versions", args=(us.id,))
+ url = "%s?id=%s" % (url, history_entry.id)
+
+ response = client.get(url, content_type="application/json")
+ assert 200 == response.status_code, response.status_code
+ assert response.data == None
From 591614e57adc877f93f9b87fced790e25188f3b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 11 May 2016 20:36:33 +0200
Subject: [PATCH 021/261] Remove USER_PERMISSIONS
---
taiga/permissions/permissions.py | 20 -----
taiga/permissions/service.py | 4 +-
.../test_attachment_resources.py | 16 ++--
.../test_history_resources.py | 6 +-
.../test_issues_custom_attributes_resource.py | 6 +-
.../test_issues_resources.py | 38 ++++-----
.../test_milestones_resources.py | 6 +-
.../test_modules_resources.py | 6 +-
.../test_resolver_resources.py | 6 +-
.../test_search_resources.py | 6 +-
.../test_tasks_custom_attributes_resource.py | 6 +-
.../test_tasks_resources.py | 6 +-
.../test_timelines_resources.py | 6 +-
..._userstories_custom_attributes_resource.py | 6 +-
.../test_userstories_resources.py | 78 +++++++++----------
.../test_wiki_resources.py | 50 ++++++------
tests/integration/test_users.py | 4 +-
tests/integration/test_watch_projects.py | 4 +-
18 files changed, 127 insertions(+), 147 deletions(-)
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index edd24618..61c0b683 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -28,26 +28,6 @@ ANON_PERMISSIONS = [
('view_wiki_links', _('View wiki links')),
]
-USER_PERMISSIONS = [
- ('view_project', _('View project')),
- ('view_milestones', _('View milestones')),
- ('view_us', _('View user stories')),
- ('view_issues', _('View issues')),
- ('view_tasks', _('View tasks')),
- ('view_wiki_pages', _('View wiki pages')),
- ('view_wiki_links', _('View wiki links')),
- ('request_membership', _('Request membership')),
- ('add_us_to_project', _('Add user story to project')),
- ('add_comments_to_us', _('Add comments to user stories')),
- ('add_comments_to_task', _('Add comments to tasks')),
- ('add_issue', _('Add issues')),
- ('add_comments_to_issue', _('Add comments to issues')),
- ('add_wiki_page', _('Add wiki page')),
- ('modify_wiki_page', _('Modify wiki page')),
- ('add_wiki_link', _('Add wiki link')),
- ('modify_wiki_link', _('Modify wiki link')),
-]
-
MEMBERS_PERMISSIONS = [
('view_project', _('View project')),
# Milestone permissions
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index 32f79fdc..02922574 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from django.apps import apps
@@ -102,7 +102,7 @@ def get_user_project_permissions(user, project, cache="user"):
if user.is_superuser:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
- public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS))
+ public_permissions = []
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
elif membership:
if membership.is_admin:
diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index 0395d8b4..c48a7e3a 100644
--- a/tests/integration/resources_permissions/test_attachment_resources.py
+++ b/tests/integration/resources_permissions/test_attachment_resources.py
@@ -4,7 +4,7 @@ from django.test.client import MULTIPART_CONTENT
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.projects.attachments.serializers import AttachmentSerializer
@@ -38,11 +38,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
@@ -491,9 +491,9 @@ def test_wiki_attachment_patch(client, data, data_wiki):
attachment_data = json.dumps(attachment_data)
results = helper_test_http_method(client, 'patch', public_url, attachment_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users)
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users)
@@ -583,9 +583,9 @@ def test_wiki_attachment_delete(client, data, data_wiki):
]
results = helper_test_http_method(client, 'delete', public_url, None, [None, data.registered_user])
- assert results == [401, 204]
+ assert results == [401, 403]
results = helper_test_http_method(client, 'delete', private_url1, None, [None, data.registered_user])
- assert results == [401, 204]
+ assert results == [401, 403]
results = helper_test_http_method(client, 'delete', private_url2, None, users)
assert results == [401, 403, 403, 204]
results = helper_test_http_method(client, 'delete', blocked_url, None, users)
@@ -721,7 +721,7 @@ def test_wiki_attachment_create(client, data, data_wiki):
content_type=MULTIPART_CONTENT,
after_each_request=_after_each_request_hook)
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
attachment_data = {"description": "test",
"object_id": data_wiki.blocked_wiki_attachment.object_id,
diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py
index be17510c..9d39b9d7 100644
--- a/tests/integration/resources_permissions/test_history_resources.py
+++ b/tests/integration/resources_permissions/test_history_resources.py
@@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse
from django.utils import timezone
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.projects.history.services import make_key_from_model_object
@@ -34,11 +34,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
index 2c90cbfc..718172df 100644
--- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
@@ -22,7 +22,7 @@ from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
- ANON_PERMISSIONS, USER_PERMISSIONS)
+ ANON_PERMISSIONS)
from tests import factories as f
from tests.utils import helper_test_http_method
@@ -43,11 +43,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 2a6d3974..4cf3e2a1 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse
from taiga.projects import choices as project_choices
from taiga.projects.issues.serializers import IssueSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json
from tests import factories as f
@@ -39,12 +39,12 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
m.private_project2 = f.ProjectFactory(is_private=True,
@@ -402,7 +402,7 @@ def test_issue_create(client, data):
"type": data.public_project.issue_types.all()[0].pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"subject": "test",
@@ -414,7 +414,7 @@ def test_issue_create(client, data):
"type": data.private_project1.issue_types.all()[0].pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"subject": "test",
@@ -456,21 +456,21 @@ def test_issue_patch(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- patch_data = json.dumps({"subject": "test", "version": data.public_issue.version})
- results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.public_issue.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version})
- results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version})
- results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version})
- results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
- assert results == [401, 403, 403, 451, 451]
+ patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_issue_bulk_create(client, data):
@@ -511,12 +511,12 @@ def test_issue_bulk_create(client, data):
bulk_data = json.dumps({"bulk_issues": "test1\ntest2",
"project_id": data.public_issue.project.pk})
results = helper_test_http_method(client, 'post', url, bulk_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
bulk_data = json.dumps({"bulk_issues": "test1\ntest2",
"project_id": data.private_issue1.project.pk})
results = helper_test_http_method(client, 'post', url, bulk_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
bulk_data = json.dumps({"bulk_issues": "test1\ntest2",
"project_id": data.private_issue2.project.pk})
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index 5a6f26a4..c42c0580 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -6,7 +6,7 @@ from taiga.projects import choices as project_choices
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
from taiga.projects.notifications.services import add_watcher
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -35,11 +35,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py
index 8260bd2f..27269458 100644
--- a/tests/integration/resources_permissions/test_modules_resources.py
+++ b/tests/integration/resources_permissions/test_modules_resources.py
@@ -2,7 +2,7 @@ import uuid
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json
from tests import factories as f
@@ -38,11 +38,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py
index f878ca14..6858d976 100644
--- a/tests/integration/resources_permissions/test_resolver_resources.py
+++ b/tests/integration/resources_permissions/test_resolver_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -29,12 +29,12 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
slug="public")
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
slug="private1")
m.private_project2 = f.ProjectFactory(is_private=True,
diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py
index 8d3d9442..783818d3 100644
--- a/tests/integration/resources_permissions/test_search_resources.py
+++ b/tests/integration/resources_permissions/test_search_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals
@@ -29,11 +29,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
index 1fd33e46..44509354 100644
--- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
@@ -22,7 +22,7 @@ from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
- ANON_PERMISSIONS, USER_PERMISSIONS)
+ ANON_PERMISSIONS)
from tests import factories as f
from tests.utils import helper_test_http_method
@@ -43,11 +43,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 4771d12c..274f7a0c 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.tasks.serializers import TaskSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
@@ -39,12 +39,12 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
m.private_project2 = f.ProjectFactory(is_private=True,
diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py
index 0a874443..ffc4e41d 100644
--- a/tests/integration/resources_permissions/test_timelines_resources.py
+++ b/tests/integration/resources_permissions/test_timelines_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@@ -29,11 +29,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
index 9e6bd6ff..030060f3 100644
--- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
@@ -22,7 +22,7 @@ from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
- ANON_PERMISSIONS, USER_PERMISSIONS)
+ ANON_PERMISSIONS)
from tests import factories as f
@@ -44,11 +44,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 80559a87..0daadad5 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.userstories.serializers import UserStorySerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
@@ -39,12 +39,12 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
m.private_project2 = f.ProjectFactory(is_private=True,
@@ -177,29 +177,29 @@ def test_user_story_update(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- user_story_data = UserStorySerializer(data.public_user_story).data
- user_story_data["subject"] = "test"
- user_story_data = json.dumps(user_story_data)
- results = helper_test_http_method(client, 'put', public_url, user_story_data, users)
- assert results == [401, 403, 403, 200, 200]
+ user_story_data = UserStorySerializer(data.public_user_story).data
+ user_story_data["subject"] = "test"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', public_url, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
- user_story_data = UserStorySerializer(data.private_user_story1).data
- user_story_data["subject"] = "test"
- user_story_data = json.dumps(user_story_data)
- results = helper_test_http_method(client, 'put', private_url1, user_story_data, users)
- assert results == [401, 403, 403, 200, 200]
+ user_story_data = UserStorySerializer(data.private_user_story1).data
+ user_story_data["subject"] = "test"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url1, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
- user_story_data = UserStorySerializer(data.private_user_story2).data
- user_story_data["subject"] = "test"
- user_story_data = json.dumps(user_story_data)
- results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
- assert results == [401, 403, 403, 200, 200]
+ user_story_data = UserStorySerializer(data.private_user_story2).data
+ user_story_data["subject"] = "test"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
- user_story_data = UserStorySerializer(data.blocked_user_story).data
- user_story_data["subject"] = "test"
- user_story_data = json.dumps(user_story_data)
- results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users)
- assert results == [401, 403, 403, 451, 451]
+ user_story_data = UserStorySerializer(data.blocked_user_story).data
+ user_story_data["subject"] = "test"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_user_story_update_with_project_change(client):
user1 = f.UserFactory.create()
@@ -361,11 +361,11 @@ def test_user_story_create(client, data):
create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk})
results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk})
results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk})
results = helper_test_http_method(client, 'post', url, create_data, users)
@@ -391,21 +391,21 @@ def test_user_story_patch(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version})
- results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version})
- results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version})
- results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version})
- results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
- assert results == [401, 403, 403, 451, 451]
+ patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_user_story_action_bulk_create(client, data):
@@ -421,11 +421,11 @@ def test_user_story_action_bulk_create(client, data):
bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.public_user_story.project.pk})
results = helper_test_http_method(client, 'post', url, bulk_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story1.project.pk})
results = helper_test_http_method(client, 'post', url, bulk_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story2.project.pk})
results = helper_test_http_method(client, 'post', url, bulk_data, users)
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index c69fb2bc..ed76e769 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -1,7 +1,7 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
@@ -37,11 +37,11 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
@@ -154,13 +154,13 @@ def test_wiki_page_update(client, data):
wiki_page_data["content"] = "test"
wiki_page_data = json.dumps(wiki_page_data)
results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data
wiki_page_data["content"] = "test"
wiki_page_data = json.dumps(wiki_page_data)
results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data
wiki_page_data["content"] = "test"
@@ -244,7 +244,7 @@ def test_wiki_page_create(client, data):
"project": data.public_project.pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete())
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"content": "test",
@@ -252,7 +252,7 @@ def test_wiki_page_create(client, data):
"project": data.private_project1.pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete())
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"content": "test",
@@ -287,11 +287,11 @@ def test_wiki_page_patch(client, data):
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version})
results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
@@ -361,13 +361,13 @@ def test_wiki_link_update(client, data):
wiki_link_data["title"] = "test"
wiki_link_data = json.dumps(wiki_link_data)
results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data
wiki_link_data["title"] = "test"
wiki_link_data = json.dumps(wiki_link_data)
results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users)
- assert results == [401, 200, 200, 200, 200]
+ assert results == [401, 403, 403, 200, 200]
wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data
wiki_link_data["title"] = "test"
@@ -450,7 +450,7 @@ def test_wiki_link_create(client, data):
"project": data.public_project.pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"title": "test",
@@ -458,7 +458,7 @@ def test_wiki_link_create(client, data):
"project": data.private_project1.pk,
})
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 201, 201, 201, 201]
+ assert results == [401, 403, 403, 201, 201]
create_data = json.dumps({
"title": "test",
@@ -492,21 +492,21 @@ def test_wiki_link_patch(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 200, 200, 200, 200]
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 200, 200, 200, 200]
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
- assert results == [401, 403, 403, 451, 451]
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_wikipage_action_watch(client, data):
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 4e9da6e5..7ca9b867 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -14,7 +14,7 @@ from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.users import models
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
from taiga.auth.tokens import get_token_for_user
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.users.services import get_watched_list, get_voted_list, get_liked_list
from taiga.projects.notifications.choices import NotifyLevel
@@ -340,7 +340,7 @@ def test_list_contacts_no_projects(client):
def test_list_contacts_public_projects(client):
project = f.ProjectFactory.create(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)))
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)))
user_1 = f.UserFactory.create()
user_2 = f.UserFactory.create()
diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py
index 2608864b..6c4f7d41 100644
--- a/tests/integration/test_watch_projects.py
+++ b/tests/integration/test_watch_projects.py
@@ -20,7 +20,7 @@ import pytest
import json
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from .. import factories as f
@@ -129,7 +129,7 @@ def test_get_project_is_watcher(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(is_private=False,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
- public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)))
+ public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)))
url_detail = reverse("projects-detail", args=(project.id,))
url_watch = reverse("projects-watch", args=(project.id,))
From 38e5198cc9443fb1d6c1c67a99d7c5dfafee6f27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 23 May 2016 14:57:52 +0200
Subject: [PATCH 022/261] Minor refactor over permissions module
---
...rate_fixtures_initial_project_templates.sh | 6 ++
taiga/base/api/permissions.py | 33 ++------
taiga/permissions/choices.py | 72 ++++++++++++++++
taiga/permissions/permissions.py | 83 +++++++------------
taiga/permissions/{service.py => services.py} | 9 +-
taiga/projects/api.py | 12 +--
taiga/projects/history/permissions.py | 2 +-
taiga/projects/issues/permissions.py | 14 +---
.../management/commands/sample_data.py | 2 +-
taiga/projects/models.py | 11 ++-
taiga/projects/notifications/services.py | 2 +-
taiga/projects/permissions.py | 25 +++---
taiga/projects/references/api.py | 2 +-
taiga/projects/serializers.py | 4 +-
taiga/projects/signals.py | 1 -
taiga/projects/tasks/permissions.py | 5 +-
taiga/projects/userstories/api.py | 1 -
taiga/projects/userstories/permissions.py | 7 +-
taiga/searches/api.py | 2 +-
taiga/timeline/permissions.py | 8 +-
taiga/users/models.py | 2 +-
taiga/webhooks/permissions.py | 2 +-
tests/factories.py | 2 +-
.../test_attachment_resources.py | 2 +-
.../test_history_resources.py | 2 +-
.../test_issues_custom_attributes_resource.py | 2 +-
.../test_issues_resources.py | 2 +-
.../test_milestones_resources.py | 2 +-
.../test_modules_resources.py | 2 +-
.../test_projects_choices_resources.py | 2 +-
.../test_projects_resource.py | 2 +-
.../test_resolver_resources.py | 2 +-
.../test_search_resources.py | 2 +-
.../test_tasks_custom_attributes_resource.py | 2 +-
.../test_tasks_resources.py | 2 +-
.../test_timelines_resources.py | 2 +-
..._userstories_custom_attributes_resource.py | 2 +-
.../test_userstories_resources.py | 2 +-
.../test_wiki_resources.py | 2 +-
tests/integration/test_notifications.py | 2 +-
tests/integration/test_permissions.py | 36 ++++----
tests/integration/test_projects.py | 2 +-
tests/integration/test_searches.py | 2 +-
tests/integration/test_users.py | 2 +-
tests/integration/test_watch_projects.py | 2 +-
tests/unit/test_permissions.py | 26 ------
46 files changed, 205 insertions(+), 206 deletions(-)
create mode 100755 scripts/generate_fixtures_initial_project_templates.sh
create mode 100644 taiga/permissions/choices.py
rename taiga/permissions/{service.py => services.py} (96%)
delete mode 100644 tests/unit/test_permissions.py
diff --git a/scripts/generate_fixtures_initial_project_templates.sh b/scripts/generate_fixtures_initial_project_templates.sh
new file mode 100755
index 00000000..d0201489
--- /dev/null
+++ b/scripts/generate_fixtures_initial_project_templates.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+python ./manage.py dumpdata --format json \
+ --indent 4 \
+ --output './taiga/projects/fixtures/initial_project_templates.json' \
+ 'projects.ProjectTemplate'
diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py
index 62b40619..19b366fc 100644
--- a/taiga/base/api/permissions.py
+++ b/taiga/base/api/permissions.py
@@ -20,11 +20,12 @@ import abc
from functools import reduce
from taiga.base.utils import sequence as sq
-from taiga.permissions.service import user_has_perm, is_project_admin
+from taiga.permissions.services import user_has_perm, is_project_admin
from django.apps import apps
from django.utils.translation import ugettext as _
+
######################################################################
# Base permissiones definition
######################################################################
@@ -179,33 +180,6 @@ class HasProjectPerm(PermissionComponent):
return user_has_perm(request.user, self.project_perm, obj)
-class HasProjectParamAndPerm(PermissionComponent):
- def __init__(self, perm, *components):
- self.project_perm = perm
- super().__init__(*components)
-
- def check_permissions(self, request, view, obj=None):
- Project = apps.get_model('projects', 'Project')
- project_id = request.QUERY_PARAMS.get("project", None)
- try:
- project = Project.objects.get(pk=project_id)
- except Project.DoesNotExist:
- return False
- return user_has_perm(request.user, self.project_perm, project)
-
-
-class HasMandatoryParam(PermissionComponent):
- def __init__(self, param, *components):
- self.mandatory_param = param
- super().__init__(*components)
-
- def check_permissions(self, request, view, obj=None):
- param = request.GET.get(self.mandatory_param, None)
- if param:
- return True
- return False
-
-
class IsProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
return is_project_admin(request.user, obj)
@@ -213,6 +187,9 @@ class IsProjectAdmin(PermissionComponent):
class IsObjectOwner(PermissionComponent):
def check_permissions(self, request, view, obj=None):
+ if obj.owner is None:
+ return False
+
return obj.owner == request.user
diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py
new file mode 100644
index 00000000..bfd7192e
--- /dev/null
+++ b/taiga/permissions/choices.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext_lazy as _
+
+ANON_PERMISSIONS = [
+ ('view_project', _('View project')),
+ ('view_milestones', _('View milestones')),
+ ('view_us', _('View user stories')),
+ ('view_tasks', _('View tasks')),
+ ('view_issues', _('View issues')),
+ ('view_wiki_pages', _('View wiki pages')),
+ ('view_wiki_links', _('View wiki links')),
+]
+
+MEMBERS_PERMISSIONS = [
+ ('view_project', _('View project')),
+ # Milestone permissions
+ ('view_milestones', _('View milestones')),
+ ('add_milestone', _('Add milestone')),
+ ('modify_milestone', _('Modify milestone')),
+ ('delete_milestone', _('Delete milestone')),
+ # US permissions
+ ('view_us', _('View user story')),
+ ('add_us', _('Add user story')),
+ ('modify_us', _('Modify user story')),
+ ('delete_us', _('Delete user story')),
+ # Task permissions
+ ('view_tasks', _('View tasks')),
+ ('add_task', _('Add task')),
+ ('modify_task', _('Modify task')),
+ ('delete_task', _('Delete task')),
+ # Issue permissions
+ ('view_issues', _('View issues')),
+ ('add_issue', _('Add issue')),
+ ('modify_issue', _('Modify issue')),
+ ('delete_issue', _('Delete issue')),
+ # Wiki page permissions
+ ('view_wiki_pages', _('View wiki pages')),
+ ('add_wiki_page', _('Add wiki page')),
+ ('modify_wiki_page', _('Modify wiki page')),
+ ('delete_wiki_page', _('Delete wiki page')),
+ # Wiki link permissions
+ ('view_wiki_links', _('View wiki links')),
+ ('add_wiki_link', _('Add wiki link')),
+ ('modify_wiki_link', _('Modify wiki link')),
+ ('delete_wiki_link', _('Delete wiki link')),
+]
+
+ADMINS_PERMISSIONS = [
+ ('modify_project', _('Modify project')),
+ ('delete_project', _('Delete project')),
+ ('add_member', _('Add member')),
+ ('remove_member', _('Remove member')),
+ ('admin_project_values', _('Admin project values')),
+ ('admin_roles', _('Admin roles')),
+]
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index 61c0b683..4e563522 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -16,57 +16,38 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext_lazy as _
+from django.apps import apps
-ANON_PERMISSIONS = [
- ('view_project', _('View project')),
- ('view_milestones', _('View milestones')),
- ('view_us', _('View user stories')),
- ('view_tasks', _('View tasks')),
- ('view_issues', _('View issues')),
- ('view_wiki_pages', _('View wiki pages')),
- ('view_wiki_links', _('View wiki links')),
-]
+from taiga.base.api.permissions import PermissionComponent
-MEMBERS_PERMISSIONS = [
- ('view_project', _('View project')),
- # Milestone permissions
- ('view_milestones', _('View milestones')),
- ('add_milestone', _('Add milestone')),
- ('modify_milestone', _('Modify milestone')),
- ('delete_milestone', _('Delete milestone')),
- # US permissions
- ('view_us', _('View user story')),
- ('add_us', _('Add user story')),
- ('modify_us', _('Modify user story')),
- ('delete_us', _('Delete user story')),
- # Task permissions
- ('view_tasks', _('View tasks')),
- ('add_task', _('Add task')),
- ('modify_task', _('Modify task')),
- ('delete_task', _('Delete task')),
- # Issue permissions
- ('view_issues', _('View issues')),
- ('add_issue', _('Add issue')),
- ('modify_issue', _('Modify issue')),
- ('delete_issue', _('Delete issue')),
- # Wiki page permissions
- ('view_wiki_pages', _('View wiki pages')),
- ('add_wiki_page', _('Add wiki page')),
- ('modify_wiki_page', _('Modify wiki page')),
- ('delete_wiki_page', _('Delete wiki page')),
- # Wiki link permissions
- ('view_wiki_links', _('View wiki links')),
- ('add_wiki_link', _('Add wiki link')),
- ('modify_wiki_link', _('Modify wiki link')),
- ('delete_wiki_link', _('Delete wiki link')),
-]
+from . import services
-ADMINS_PERMISSIONS = [
- ('modify_project', _('Modify project')),
- ('add_member', _('Add member')),
- ('remove_member', _('Remove member')),
- ('delete_project', _('Delete project')),
- ('admin_project_values', _('Admin project values')),
- ('admin_roles', _('Admin roles')),
-]
+
+######################################################################
+# Generic perms
+######################################################################
+
+class HasProjectPerm(PermissionComponent):
+ def __init__(self, perm, *components):
+ self.project_perm = perm
+ super().__init__(*components)
+
+ def check_permissions(self, request, view, obj=None):
+ return services.user_has_perm(request.user, self.project_perm, obj)
+
+
+class IsObjectOwner(PermissionComponent):
+ def check_permissions(self, request, view, obj=None):
+ if obj.owner is None:
+ return False
+
+ return obj.owner == request.user
+
+
+######################################################################
+# Project Perms
+######################################################################
+
+class IsProjectAdmin(PermissionComponent):
+ def check_permissions(self, request, view, obj=None):
+ return services.is_project_admin(request.user, obj)
diff --git a/taiga/permissions/service.py b/taiga/permissions/services.py
similarity index 96%
rename from taiga/permissions/service.py
rename to taiga/permissions/services.py
index 02922574..9ed1e8c3 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/services.py
@@ -16,10 +16,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from .choices import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from django.apps import apps
+
def _get_user_project_membership(user, project, cache="user"):
"""
cache param determines how memberships are calculated trying to reuse the existing data
@@ -83,10 +84,6 @@ def user_has_perm(user, perm, obj=None, cache="user"):
return perm in get_user_project_permissions(user, project, cache=cache)
-def role_has_perm(role, perm):
- return perm in role.permissions
-
-
def _get_membership_permissions(membership):
if membership and membership.role and membership.role.permissions:
return membership.role.permissions
@@ -97,7 +94,7 @@ def get_user_project_permissions(user, project, cache="user"):
"""
cache param determines how memberships are calculated trying to reuse the existing data
in cache
- """
+ """
membership = _get_user_project_membership(user, project, cache=cache)
if user.is_superuser:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index a7c3aa61..17684fd3 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -51,8 +51,8 @@ from taiga.projects.userstories.models import UserStory, RolePoints
from taiga.projects.tasks.models import Task
from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
-from taiga.permissions import service as permissions_service
-from taiga.users import services as users_service
+from taiga.permissions import services as permissions_services
+from taiga.users import services as users_services
from . import filters as project_filters
from . import models
@@ -147,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
else:
project = self.get_object()
- if permissions_service.is_project_admin(self.request.user, project):
+ if permissions_services.is_project_admin(self.request.user, project):
serializer_class = self.admin_serializer_class
return serializer_class
@@ -415,7 +415,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
update_permissions = True
if update_permissions:
- permissions_service.set_base_permissions_for_project(obj)
+ permissions_services.set_base_permissions_for_project(obj)
def pre_save(self, obj):
if not obj.id:
@@ -603,12 +603,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
use_admin_serializer = True
if self.action == "retrieve":
- use_admin_serializer = permissions_service.is_project_admin(self.request.user, self.object.project)
+ use_admin_serializer = permissions_services.is_project_admin(self.request.user, self.object.project)
project_id = self.request.QUERY_PARAMS.get("project", None)
if self.action == "list" and project_id is not None:
project = get_object_or_404(models.Project, pk=project_id)
- use_admin_serializer = permissions_service.is_project_admin(self.request.user, project)
+ use_admin_serializer = permissions_services.is_project_admin(self.request.user, project)
if use_admin_serializer:
return self.admin_serializer_class
diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py
index fdce68cd..54fc59f1 100644
--- a/taiga/projects/history/permissions.py
+++ b/taiga/projects/history/permissions.py
@@ -19,7 +19,7 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectAdmin, AllowAny,
IsObjectOwner, PermissionComponent)
-from taiga.permissions.service import is_project_admin
+from taiga.permissions.services import is_project_admin
from taiga.projects.history.services import get_model_from_key, get_pk_from_key
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 291dede7..791848f9 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -16,9 +16,9 @@
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectAdmin, PermissionComponent,
- AllowAny, IsAuthenticated, IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+
class IssuePermission(TaigaResourcePermission):
@@ -40,14 +40,6 @@ class IssuePermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
-class HasIssueIdUrlParam(PermissionComponent):
- def check_permissions(self, request, view, obj=None):
- param = view.kwargs.get('issue_id', None)
- if param:
- return True
- return False
-
-
class IssueVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index f5c7b6ea..be43df8c 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -29,7 +29,7 @@ from django.contrib.contenttypes.models import ContentType
from sampledatahelper.helper import SampleDataHelper
from taiga.users.models import *
-from taiga.permissions.permissions import ANON_PERMISSIONS
+from taiga.permissions.choices import ANON_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_STAFF
from taiga.projects.models import *
from taiga.projects.milestones.models import *
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index cd38c35b..506c2736 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -40,7 +40,7 @@ from taiga.base.utils.sequence import arithmetic_progression
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.slug import slugify_uniquely_for_queryset
-from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
+from taiga.permissions.choices import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import (
@@ -366,7 +366,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
@cached_property
def cached_memberships(self):
- return {m.user.id: m for m in self.memberships.exclude(user__isnull=True).select_related("user", "project", "role")}
+ return {m.user.id: m for m in self.memberships.exclude(user__isnull=True)
+ .select_related("user", "project", "role")}
def cached_memberships_for_user(self, user):
return self.cached_memberships.get(user.id, None)
@@ -966,9 +967,11 @@ class ProjectTemplate(models.Model):
project=project)
if self.priorities:
- project.default_priority = Priority.objects.get(name=self.default_options["priority"], project=project)
+ project.default_priority = Priority.objects.get(name=self.default_options["priority"],
+ project=project)
if self.severities:
- project.default_severity = Severity.objects.get(name=self.default_options["severity"], project=project)
+ project.default_severity = Severity.objects.get(name=self.default_options["severity"],
+ project=project)
return project
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 22d518cb..27e4b5ae 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -35,7 +35,7 @@ from taiga.projects.history.choices import HistoryType
from taiga.projects.history.services import (make_key_from_model_object,
get_last_snapshot_for_key,
get_model_from_key)
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
from .models import HistoryChangeNotification, Watched
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index de65cd69..ba4f3260 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -18,18 +18,21 @@
from django.utils.translation import ugettext as _
from taiga.base.api.permissions import TaigaResourcePermission
-from taiga.base.api.permissions import HasProjectPerm
from taiga.base.api.permissions import IsAuthenticated
-from taiga.base.api.permissions import IsProjectAdmin
from taiga.base.api.permissions import AllowAny
from taiga.base.api.permissions import IsSuperUser
+from taiga.base.api.permissions import IsObjectOwner
from taiga.base.api.permissions import PermissionComponent
from taiga.base import exceptions as exc
-from taiga.projects.models import Membership
+from taiga.permissions.permissions import HasProjectPerm
+from taiga.permissions.permissions import IsProjectAdmin
+
+from . import models
from . import services
+
class CanLeaveProject(PermissionComponent):
def check_permissions(self, request, view, obj=None):
if not obj or not request.user.is_authenticated():
@@ -37,20 +40,12 @@ class CanLeaveProject(PermissionComponent):
try:
if not services.can_user_leave_project(request.user, obj):
- raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are no more admins"))
+ raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are "
+ "no more admins"))
return True
- except Membership.DoesNotExist:
+ except models.Membership.DoesNotExist:
return False
-class IsMainOwner(PermissionComponent):
- def check_permissions(self, request, view, obj=None):
- if not obj or not request.user.is_authenticated():
- return False
-
- if obj.owner is None:
- return False
-
- return obj.owner == request.user
class ProjectPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
@@ -79,7 +74,7 @@ class ProjectPermission(TaigaResourcePermission):
leave_perms = CanLeaveProject()
transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_request_perms = IsProjectAdmin()
- transfer_start_perms = IsMainOwner()
+ transfer_start_perms = IsObjectOwner()
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py
index 028d4642..0775bc72 100644
--- a/taiga/projects/references/api.py
+++ b/taiga/projects/references/api.py
@@ -21,7 +21,7 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.api import viewsets
from taiga.base.api.utils import get_object_or_404
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
from .serializers import ResolverSerializer
from . import permissions
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 661aeece..7096919a 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -32,8 +32,8 @@ from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.serializers import ProjectRoleSerializer
from taiga.users.validators import RoleExistsValidator
-from taiga.permissions.service import get_user_project_permissions
-from taiga.permissions.service import is_project_admin, is_project_owner
+from taiga.permissions.services import get_user_project_permissions
+from taiga.permissions.services import is_project_admin, is_project_owner
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from . import models
diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py
index 51ff6485..32da1176 100644
--- a/taiga/projects/signals.py
+++ b/taiga/projects/signals.py
@@ -67,7 +67,6 @@ def project_post_save(sender, instance, created, **kwargs):
if instance._importing:
return
-
template = getattr(instance, "creation_template", None)
if template is None:
ProjectTemplate = apps.get_model("projects", "ProjectTemplate")
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 9e189f10..566fc79c 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -15,9 +15,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectAdmin, AllowAny,
- IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
class TaskPermission(TaigaResourcePermission):
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 815560c1..745268cd 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -112,7 +112,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return super().update(request, *args, **kwargs)
-
def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related("role_points",
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 326c99fe..11aa5b73 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -15,12 +15,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectAdmin,
- AllowAny, IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
class UserStoryPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
retrieve_perms = HasProjectPerm('view_us')
create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')
update_perms = HasProjectPerm('modify_us')
diff --git a/taiga/searches/api.py b/taiga/searches/api.py
index be8a0e9a..f10554a8 100644
--- a/taiga/searches/api.py
+++ b/taiga/searches/api.py
@@ -21,7 +21,7 @@ from taiga.base.api import viewsets
from taiga.base import response
from taiga.base.api.utils import get_object_or_404
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
from . import services
from . import serializers
diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py
index b71530ed..f2753869 100644
--- a/taiga/timeline/permissions.py
+++ b/taiga/timeline/permissions.py
@@ -15,13 +15,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- AllowAny)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
class UserTimelinePermission(TaigaResourcePermission):
+ enought_perms = IsSuperUser()
+ global_perms = None
retrieve_perms = AllowAny()
class ProjectTimelinePermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
retrieve_perms = HasProjectPerm('view_project')
diff --git a/taiga/users/models.py b/taiga/users/models.py
index a0a9048b..c1cf4a3b 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -38,7 +38,7 @@ from djorm_pgarray.fields import TextArrayField
from taiga.auth.tokens import get_token_for_user
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.files import get_file_path
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel
diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py
index bc1cae61..82ca5bd7 100644
--- a/taiga/webhooks/permissions.py
+++ b/taiga/webhooks/permissions.py
@@ -18,7 +18,7 @@
from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectAdmin,
AllowAny, PermissionComponent)
-from taiga.permissions.service import is_project_admin
+from taiga.permissions.services import is_project_admin
class IsWebhookProjectAdmin(PermissionComponent):
diff --git a/tests/factories.py b/tests/factories.py
index 252ce47a..2f1ddccc 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -26,7 +26,7 @@ from .utils import DUMMY_BMP_DATA
import factory
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index c48a7e3a..357b13a7 100644
--- a/tests/integration/resources_permissions/test_attachment_resources.py
+++ b/tests/integration/resources_permissions/test_attachment_resources.py
@@ -4,7 +4,7 @@ from django.test.client import MULTIPART_CONTENT
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.projects.attachments.serializers import AttachmentSerializer
diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py
index 9d39b9d7..62ba4e77 100644
--- a/tests/integration/resources_permissions/test_history_resources.py
+++ b/tests/integration/resources_permissions/test_history_resources.py
@@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse
from django.utils import timezone
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.projects.history.services import make_key_from_model_object
diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
index 718172df..60b57eed 100644
--- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
-from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
+from taiga.permissions.choices import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS)
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 4cf3e2a1..bb532473 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse
from taiga.projects import choices as project_choices
from taiga.projects.issues.serializers import IssueSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index c42c0580..56f072f4 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -6,7 +6,7 @@ from taiga.projects import choices as project_choices
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
from taiga.projects.notifications.services import add_watcher
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py
index 27269458..fc1de642 100644
--- a/tests/integration/resources_permissions/test_modules_resources.py
+++ b/tests/integration/resources_permissions/test_modules_resources.py
@@ -2,7 +2,7 @@ import uuid
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py
index 207889f9..c26e51b5 100644
--- a/tests/integration/resources_permissions/test_projects_choices_resources.py
+++ b/tests/integration/resources_permissions/test_projects_choices_resources.py
@@ -4,7 +4,7 @@ from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects import serializers
from taiga.users.serializers import RoleSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 9bcd7a9f..a2ec130e 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -4,7 +4,7 @@ from django.apps import apps
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.serializers import ProjectDetailSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, helper_test_http_method_and_count
diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py
index 6858d976..c8634c46 100644
--- a/tests/integration/resources_permissions/test_resolver_resources.py
+++ b/tests/integration/resources_permissions/test_resolver_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py
index 783818d3..d72e7bc2 100644
--- a/tests/integration/resources_permissions/test_search_resources.py
+++ b/tests/integration/resources_permissions/test_search_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals
diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
index 44509354..2262a222 100644
--- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
-from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
+from taiga.permissions.choices import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS)
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 274f7a0c..aff81389 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.tasks.serializers import TaskSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py
index ffc4e41d..ed68f67b 100644
--- a/tests/integration/resources_permissions/test_timelines_resources.py
+++ b/tests/integration/resources_permissions/test_timelines_resources.py
@@ -1,6 +1,6 @@
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
index 030060f3..64e7324a 100644
--- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.custom_attributes import serializers
-from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
+from taiga.permissions.choices import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS)
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 0daadad5..4da7b081 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects.userstories.serializers import UserStorySerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index ed76e769..aaee4e53 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -1,7 +1,7 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index bb2cdfd8..4d341086 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -41,7 +41,7 @@ from taiga.projects.history.services import take_snapshot
from taiga.projects.issues.serializers import IssueSerializer
from taiga.projects.userstories.serializers import UserStorySerializer
from taiga.projects.tasks.serializers import TaskSerializer
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
pytestmark = pytest.mark.django_db
diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py
index ddcf9e34..7ec25929 100644
--- a/tests/integration/test_permissions.py
+++ b/tests/integration/test_permissions.py
@@ -1,6 +1,6 @@
import pytest
-from taiga.permissions import service, permissions
+from taiga.permissions import services, choices
from django.contrib.auth.models import AnonymousUser
from .. import factories
@@ -15,15 +15,15 @@ def test_get_user_project_role():
role = factories.RoleFactory()
membership = factories.MembershipFactory(user=user1, project=project, role=role)
- assert service._get_user_project_membership(user1, project) == membership
- assert service._get_user_project_membership(user2, project) is None
+ assert services._get_user_project_membership(user1, project) == membership
+ assert services._get_user_project_membership(user2, project) is None
def test_anon_get_user_project_permissions():
project = factories.ProjectFactory()
project.anon_permissions = ["test1"]
project.public_permissions = ["test2"]
- assert service.get_user_project_permissions(AnonymousUser(), project) == set(["test1"])
+ assert services.get_user_project_permissions(AnonymousUser(), project) == set(["test1"])
def test_user_get_user_project_permissions_on_public_project():
@@ -31,7 +31,7 @@ def test_user_get_user_project_permissions_on_public_project():
project = factories.ProjectFactory()
project.anon_permissions = ["test1"]
project.public_permissions = ["test2"]
- assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"])
+ assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"])
def test_user_get_user_project_permissions_on_private_project():
@@ -40,7 +40,7 @@ def test_user_get_user_project_permissions_on_private_project():
project.anon_permissions = ["test1"]
project.public_permissions = ["test2"]
project.is_private = True
- assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"])
+ assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"])
def test_owner_get_user_project_permissions():
@@ -55,7 +55,7 @@ def test_owner_get_user_project_permissions():
expected_perms = set(
["test1", "test2", "view_us"]
)
- assert service.get_user_project_permissions(user1, project) == expected_perms
+ assert services.get_user_project_permissions(user1, project) == expected_perms
def test_owner_member_get_user_project_permissions():
@@ -68,10 +68,10 @@ def test_owner_member_get_user_project_permissions():
expected_perms = set(
["test1", "test2", "test3"] +
- [x[0] for x in permissions.ADMINS_PERMISSIONS] +
- [x[0] for x in permissions.MEMBERS_PERMISSIONS]
+ [x[0] for x in choices.ADMINS_PERMISSIONS] +
+ [x[0] for x in choices.MEMBERS_PERMISSIONS]
)
- assert service.get_user_project_permissions(user1, project) == expected_perms
+ assert services.get_user_project_permissions(user1, project) == expected_perms
def test_member_get_user_project_permissions():
@@ -82,22 +82,22 @@ def test_member_get_user_project_permissions():
role = factories.RoleFactory(permissions=["test3"])
factories.MembershipFactory(user=user1, project=project, role=role)
- assert service.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"])
+ assert services.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"])
def test_anon_user_has_perm():
project = factories.ProjectFactory()
project.anon_permissions = ["test"]
- assert service.user_has_perm(AnonymousUser(), "test", project) is True
- assert service.user_has_perm(AnonymousUser(), "fail", project) is False
+ assert services.user_has_perm(AnonymousUser(), "test", project) is True
+ assert services.user_has_perm(AnonymousUser(), "fail", project) is False
def test_authenticated_user_has_perm_on_project():
user1 = factories.UserFactory()
project = factories.ProjectFactory()
project.public_permissions = ["test"]
- assert service.user_has_perm(user1, "test", project) is True
- assert service.user_has_perm(user1, "fail", project) is False
+ assert services.user_has_perm(user1, "test", project) is True
+ assert services.user_has_perm(user1, "fail", project) is False
def test_authenticated_user_has_perm_on_project_related_object():
@@ -106,10 +106,10 @@ def test_authenticated_user_has_perm_on_project_related_object():
project.public_permissions = ["test"]
us = factories.UserStoryFactory(project=project)
- assert service.user_has_perm(user1, "test", us) is True
- assert service.user_has_perm(user1, "fail", us) is False
+ assert services.user_has_perm(user1, "test", us) is True
+ assert services.user_has_perm(user1, "fail", us) is False
def test_authenticated_user_has_perm_on_invalid_object():
user1 = factories.UserFactory()
- assert service.user_has_perm(user1, "test", user1) is False
+ assert services.user_has_perm(user1, "test", user1) is False
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 6111747b..28a0dbe3 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -7,7 +7,7 @@ from django.core import signing
from taiga.base.utils import json
from taiga.projects.services import stats as stats_services
from taiga.projects.history.services import take_snapshot
-from taiga.permissions.permissions import ANON_PERMISSIONS
+from taiga.permissions.choices import ANON_PERMISSIONS
from taiga.projects.models import Project
from .. import factories as f
diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py
index 334ad405..30c3247b 100644
--- a/tests/integration/test_searches.py
+++ b/tests/integration/test_searches.py
@@ -22,7 +22,7 @@ from django.core.urlresolvers import reverse
from .. import factories as f
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
from tests.utils import disconnect_signals, reconnect_signals
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 7ca9b867..be48c553 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -14,7 +14,7 @@ from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.users import models
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
from taiga.auth.tokens import get_token_for_user
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects import choices as project_choices
from taiga.users.services import get_watched_list, get_voted_list, get_liked_list
from taiga.projects.notifications.choices import NotifyLevel
diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py
index 6c4f7d41..5a77f086 100644
--- a/tests/integration/test_watch_projects.py
+++ b/tests/integration/test_watch_projects.py
@@ -20,7 +20,7 @@ import pytest
import json
from django.core.urlresolvers import reverse
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from .. import factories as f
diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py
deleted file mode 100644
index 5ef7a93d..00000000
--- a/tests/unit/test_permissions.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (C) 2014-2016 Andrey Antukh
-# Copyright (C) 2014-2016 Jesús Espino
-# Copyright (C) 2014-2016 David Barragán
-# Copyright (C) 2014-2016 Alejandro Alonso
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-from taiga.permissions import service
-from taiga.users.models import Role
-
-
-def test_role_has_perm():
- role = Role()
- role.permissions = ["test"]
- assert service.role_has_perm(role, "test")
- assert service.role_has_perm(role, "false") is False
From 413a83808a5c1836270928707faa6bcd75b5dffe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 11 May 2016 20:50:19 +0200
Subject: [PATCH 023/261] US #4186: Permission to add comments
---
CHANGELOG.md | 7 +-
taiga/permissions/choices.py | 5 +-
taiga/permissions/permissions.py | 37 +
.../fixtures/initial_project_templates.json | 84 +-
taiga/projects/issues/permissions.py | 5 +-
.../migrations/0041_auto_20160519_1058.py | 21 +
.../migrations/0042_auto_20160525_0911.py | 67 ++
taiga/projects/tasks/permissions.py | 6 +-
taiga/projects/userstories/permissions.py | 6 +-
taiga/projects/wiki/permissions.py | 6 +-
.../migrations/0019_auto_20160519_1058.py | 21 +
.../migrations/0020_auto_20160525_1229.py | 48 ++
.../test_issues_resources.py | 532 ++++++++----
.../test_tasks_resources.py | 466 +++++++----
.../test_userstories_resources.py | 380 ++++++---
.../test_wiki_resources.py | 786 ++++++++++++------
16 files changed, 1720 insertions(+), 757 deletions(-)
create mode 100644 taiga/projects/migrations/0041_auto_20160519_1058.py
create mode 100644 taiga/projects/migrations/0042_auto_20160525_0911.py
create mode 100644 taiga/users/migrations/0019_auto_20160519_1058.py
create mode 100644 taiga/users/migrations/0020_auto_20160525_1229.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eca3114a..97b090ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,13 @@
# Changelog #
## 2.2.0 ??? (unreleased)
+
### Features
-- [API] edit comment endpoint: comment owners and project admins can edit existing comments
+- Now comment owners and project admins can edit existing comments with the history Entry endpoint.
+- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose.
+
+### Misc
+- Lots of small and not so small bugfixes.
## 2.1.0 Ursus Americanus (2016-05-03)
diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py
index bfd7192e..f617da97 100644
--- a/taiga/permissions/choices.py
+++ b/taiga/permissions/choices.py
@@ -27,7 +27,6 @@ ANON_PERMISSIONS = [
('view_wiki_pages', _('View wiki pages')),
('view_wiki_links', _('View wiki links')),
]
-
MEMBERS_PERMISSIONS = [
('view_project', _('View project')),
# Milestone permissions
@@ -39,21 +38,25 @@ MEMBERS_PERMISSIONS = [
('view_us', _('View user story')),
('add_us', _('Add user story')),
('modify_us', _('Modify user story')),
+ ('comment_us', _('Comment user story')),
('delete_us', _('Delete user story')),
# Task permissions
('view_tasks', _('View tasks')),
('add_task', _('Add task')),
('modify_task', _('Modify task')),
+ ('comment_task', _('Comment task')),
('delete_task', _('Delete task')),
# Issue permissions
('view_issues', _('View issues')),
('add_issue', _('Add issue')),
('modify_issue', _('Modify issue')),
+ ('comment_issue', _('Comment issue')),
('delete_issue', _('Delete issue')),
# Wiki page permissions
('view_wiki_pages', _('View wiki pages')),
('add_wiki_page', _('Add wiki page')),
('modify_wiki_page', _('Modify wiki page')),
+ ('comment_wiki_page', _('Comment wiki page')),
('delete_wiki_page', _('Delete wiki page')),
# Wiki link permissions
('view_wiki_links', _('View wiki links')),
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index 4e563522..c2b7699a 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -51,3 +51,40 @@ class IsObjectOwner(PermissionComponent):
class IsProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
return services.is_project_admin(request.user, obj)
+
+
+######################################################################
+# Common perms for stories, tasks and issues
+######################################################################
+
+class CommentAndOrUpdatePerm(PermissionComponent):
+ def __init__(self, update_perm, comment_perm, *components):
+ self.update_perm = update_perm
+ self.comment_perm = comment_perm
+ super().__init__(*components)
+
+ def check_permissions(self, request, view, obj=None):
+ if not obj:
+ return False
+
+ project_id = request.DATA.get('project', None)
+ if project_id and obj.project_id != project_id:
+ project = apps.get_model("projects", "Project").objects.get(pk=project_id)
+ else:
+ project = obj.project
+
+ data_keys = request.DATA.keys()
+
+ if (not services.user_has_perm(request.user, self.comment_perm, project) and
+ "comment" in data_keys):
+ # User can't comment but there is a comment in the request
+ #raise exc.PermissionDenied(_("You don't have permissions to comment this."))
+ return False
+
+ if (not services.user_has_perm(request.user, self.update_perm, project) and
+ len(data_keys - "comment")):
+ # User can't update but there is a change in the request
+ #raise exc.PermissionDenied(_("You don't have permissions to update this."))
+ return False
+
+ return True
diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json
index 46d369a5..54e73bd0 100644
--- a/taiga/projects/fixtures/initial_project_templates.json
+++ b/taiga/projects/fixtures/initial_project_templates.json
@@ -1,56 +1,56 @@
[
{
"model": "projects.projecttemplate",
+ "pk": 1,
"fields": {
- "is_issues_activated": true,
- "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff9900\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#ffcc00\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#999999\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
- "is_backlog_activated": true,
- "modified_date": "2014-07-25T10:02:46.479Z",
- "us_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff8a84\", \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\"}, {\"color\": \"#ff9900\", \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#fcc000\", \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\"}, {\"color\": \"#5c3566\", \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\"}]",
- "is_wiki_activated": true,
- "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
- "points": "[{\"value\": null, \"order\": 1, \"name\": \"?\"}, {\"value\": 0.0, \"order\": 2, \"name\": \"0\"}, {\"value\": 0.5, \"order\": 3, \"name\": \"1/2\"}, {\"value\": 1.0, \"order\": 4, \"name\": \"1\"}, {\"value\": 2.0, \"order\": 5, \"name\": \"2\"}, {\"value\": 3.0, \"order\": 6, \"name\": \"3\"}, {\"value\": 5.0, \"order\": 7, \"name\": \"5\"}, {\"value\": 8.0, \"order\": 8, \"name\": \"8\"}, {\"value\": 10.0, \"order\": 9, \"name\": \"10\"}, {\"value\": 13.0, \"order\": 10, \"name\": \"13\"}, {\"value\": 20.0, \"order\": 11, \"name\": \"20\"}, {\"value\": 40.0, \"order\": 12, \"name\": \"40\"}]",
- "severities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#669933\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#0000FF\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#FFA500\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]",
- "is_kanban_activated": false,
- "priorities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#669933\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]",
- "created_date": "2014-04-22T14:48:43.596Z",
- "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}",
+ "name": "Scrum",
"slug": "scrum",
- "videoconferences_extra_data": "",
- "issue_statuses": "[{\"color\": \"#8C2318\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#5E8C6A\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#88A65E\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#BFB35A\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#89BAB4\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#CC0000\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#666666\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]",
- "default_owner_role": "product-owner",
- "issue_types": "[{\"color\": \"#89BAB4\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#ba89a8\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#89a8ba\", \"order\": 3, \"name\": \"Enhancement\"}]",
- "videoconferences": null,
"description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers",
- "name": "Scrum"
- },
- "pk": 1
+ "created_date": "2014-04-22T14:48:43.596Z",
+ "modified_date": "2014-07-25T10:02:46.479Z",
+ "default_owner_role": "product-owner",
+ "is_backlog_activated": true,
+ "is_kanban_activated": false,
+ "is_wiki_activated": true,
+ "is_issues_activated": true,
+ "videoconferences": null,
+ "videoconferences_extra_data": "",
+ "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}",
+ "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#669900\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]",
+ "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]",
+ "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#ff9900\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#ffcc00\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#669900\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#999999\", \"is_closed\": false}]",
+ "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#8C2318\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#5E8C6A\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#88A65E\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#BFB35A\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#89BAB4\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#CC0000\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#666666\", \"is_closed\": false}]",
+ "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#89BAB4\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#ba89a8\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#89a8ba\"}]",
+ "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#666666\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#669933\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]",
+ "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#666666\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#669933\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#0000FF\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#FFA500\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]",
+ "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]"
+ }
},
{
"model": "projects.projecttemplate",
+ "pk": 2,
"fields": {
- "is_issues_activated": false,
- "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
- "is_backlog_activated": false,
- "modified_date": "2014-07-25T13:11:42.754Z",
- "us_statuses": "[{\"wip_limit\": null, \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"name\": \"New\", \"slug\": \"new\"}, {\"wip_limit\": null, \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"name\": \"Ready\", \"slug\": \"ready\"}, {\"wip_limit\": null, \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"wip_limit\": null, \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"wip_limit\": null, \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"name\": \"Done\", \"slug\": \"done\"}, {\"wip_limit\": null, \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"name\": \"Archived\", \"slug\": \"archived\"}]",
- "is_wiki_activated": false,
- "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
- "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
- "severities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#f57900\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]",
- "is_kanban_activated": true,
- "priorities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]",
- "created_date": "2014-04-22T14:50:19.738Z",
- "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}",
+ "name": "Kanban",
"slug": "kanban",
- "videoconferences_extra_data": "",
- "issue_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#d3d7cf\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#75507b\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]",
- "default_owner_role": "product-owner",
- "issue_types": "[{\"color\": \"#cc0000\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Enhancement\"}]",
- "videoconferences": null,
"description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.",
- "name": "Kanban"
- },
- "pk": 2
+ "created_date": "2014-04-22T14:50:19.738Z",
+ "modified_date": "2014-07-25T13:11:42.754Z",
+ "default_owner_role": "product-owner",
+ "is_backlog_activated": false,
+ "is_kanban_activated": true,
+ "is_wiki_activated": false,
+ "is_issues_activated": false,
+ "videoconferences": null,
+ "videoconferences_extra_data": "",
+ "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}",
+ "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#f57900\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#729fcf\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#4e9a06\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#cc0000\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]",
+ "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]",
+ "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}]",
+ "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#d3d7cf\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#75507b\", \"is_closed\": false}]",
+ "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#cc0000\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#4e9a06\"}]",
+ "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#999999\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]",
+ "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#999999\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#f57900\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]",
+ "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]"
+ }
}
]
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 791848f9..ea823fcc 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -19,6 +19,7 @@
from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
class IssuePermission(TaigaResourcePermission):
@@ -26,8 +27,8 @@ class IssuePermission(TaigaResourcePermission):
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
create_perms = HasProjectPerm('add_issue')
- update_perms = HasProjectPerm('modify_issue')
- partial_update_perms = HasProjectPerm('modify_issue')
+ update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue')
destroy_perms = HasProjectPerm('delete_issue')
list_perms = AllowAny()
filters_data_perms = AllowAny()
diff --git a/taiga/projects/migrations/0041_auto_20160519_1058.py b/taiga/projects/migrations/0041_auto_20160519_1058.py
new file mode 100644
index 00000000..c4b0a2fd
--- /dev/null
+++ b/taiga/projects/migrations/0041_auto_20160519_1058.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-19 10:58
+from __future__ import unicode_literals
+
+from django.db import migrations
+import djorm_pgarray.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0040_remove_memberships_of_cancelled_users_acounts'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='public_permissions',
+ field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='user permissions'),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0042_auto_20160525_0911.py b/taiga/projects/migrations/0042_auto_20160525_0911.py
new file mode 100644
index 00000000..0652df06
--- /dev/null
+++ b/taiga/projects/migrations/0042_auto_20160525_0911.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-25 09:11
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+UPDATE_PROJECTS_ANON_PERMISSIONS_SQL = """
+ UPDATE projects_project
+ SET
+ ANON_PERMISSIONS = array_append(ANON_PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(ANON_PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(ANON_PERMISSIONS)
+"""
+
+UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL = """
+ UPDATE projects_project
+ SET
+ PUBLIC_PERMISSIONS = array_append(PUBLIC_PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(PUBLIC_PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(PUBLIC_PERMISSIONS)
+"""
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0041_auto_20160519_1058'),
+ ]
+
+ operations = [
+ # user stories
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ # tasks
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ # issues
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ )
+ ]
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 566fc79c..8dfafc41 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -18,14 +18,16 @@
from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
+
class TaskPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
create_perms = HasProjectPerm('add_task')
- update_perms = HasProjectPerm('modify_task')
- partial_update_perms = HasProjectPerm('modify_task')
+ update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task')
destroy_perms = HasProjectPerm('delete_task')
list_perms = AllowAny()
csv_perms = AllowAny()
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 11aa5b73..c91ef2a7 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -18,14 +18,16 @@
from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
+
class UserStoryPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')
- update_perms = HasProjectPerm('modify_us')
- partial_update_perms = HasProjectPerm('modify_us')
+ update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us')
destroy_perms = HasProjectPerm('delete_us')
list_perms = AllowAny()
filters_data_perms = AllowAny()
diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py
index d85d56ea..e60458c7 100644
--- a/taiga/projects/wiki/permissions.py
+++ b/taiga/projects/wiki/permissions.py
@@ -19,6 +19,8 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsAuthenticated, IsProjectAdmin, AllowAny,
IsSuperUser)
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
+
class WikiPagePermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
@@ -26,8 +28,8 @@ class WikiPagePermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_wiki_pages')
by_slug_perms = HasProjectPerm('view_wiki_pages')
create_perms = HasProjectPerm('add_wiki_page')
- update_perms = HasProjectPerm('modify_wiki_page')
- partial_update_perms = HasProjectPerm('modify_wiki_page')
+ update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page')
destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny()
render_perms = AllowAny()
diff --git a/taiga/users/migrations/0019_auto_20160519_1058.py b/taiga/users/migrations/0019_auto_20160519_1058.py
new file mode 100644
index 00000000..69780084
--- /dev/null
+++ b/taiga/users/migrations/0019_auto_20160519_1058.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-19 10:58
+from __future__ import unicode_literals
+
+from django.db import migrations
+import djorm_pgarray.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0018_remove_vote_issues_in_roles_permissions_field'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='permissions',
+ field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='permissions'),
+ ),
+ ]
diff --git a/taiga/users/migrations/0020_auto_20160525_1229.py b/taiga/users/migrations/0020_auto_20160525_1229.py
new file mode 100644
index 00000000..765eb3e1
--- /dev/null
+++ b/taiga/users/migrations/0020_auto_20160525_1229.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-25 12:29
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+UPDATE_ROLES_PERMISSIONS_SQL = """
+ UPDATE users_role
+ SET
+ PERMISSIONS = array_append(PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(PERMISSIONS)
+"""
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0019_auto_20160519_1058'),
+ ]
+
+ operations = [
+ # user stories
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ # tasks
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ # issues
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ ),
+
+ # issues
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ )
+ ]
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index bb532473..536b4422 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -132,6 +132,55 @@ def data():
return m
+def test_issue_list(client, data):
+ url = reverse('issues-list')
+
+ response = client.get(url)
+ issues_data = json.loads(response.content.decode('utf-8'))
+ assert len(issues_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.registered_user)
+
+ response = client.get(url)
+ issues_data = json.loads(response.content.decode('utf-8'))
+ assert len(issues_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_member_with_perms)
+
+ response = client.get(url)
+ issues_data = json.loads(response.content.decode('utf-8'))
+ assert len(issues_data) == 4
+ assert response.status_code == 200
+
+ client.login(data.project_owner)
+
+ response = client.get(url)
+ issues_data = json.loads(response.content.decode('utf-8'))
+ assert len(issues_data) == 4
+ assert response.status_code == 200
+
+
+def test_issue_list_filter_by_project_ok(client, data):
+ url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk)
+
+ client.login(data.project_owner)
+ response = client.get(url)
+
+ assert response.status_code == 200
+ assert len(response.data) == 1
+
+
+def test_issue_list_filter_by_project_error(client, data):
+ url = "{}?project={}".format(reverse("issues-list"), "-ERROR-")
+
+ client.login(data.project_owner)
+ response = client.get(url)
+
+ assert response.status_code == 400
+
+
def test_issue_retrieve(client, data):
public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
@@ -156,7 +205,67 @@ def test_issue_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_issue_update(client, data):
+def test_issue_create(client, data):
+ url = reverse('issues-list')
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 1,
+ "project": data.public_project.pk,
+ "severity": data.public_project.severities.all()[0].pk,
+ "priority": data.public_project.priorities.all()[0].pk,
+ "status": data.public_project.issue_statuses.all()[0].pk,
+ "type": data.public_project.issue_types.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 2,
+ "project": data.private_project1.pk,
+ "severity": data.private_project1.severities.all()[0].pk,
+ "priority": data.private_project1.priorities.all()[0].pk,
+ "status": data.private_project1.issue_statuses.all()[0].pk,
+ "type": data.private_project1.issue_types.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 3,
+ "project": data.private_project2.pk,
+ "severity": data.private_project2.severities.all()[0].pk,
+ "priority": data.private_project2.priorities.all()[0].pk,
+ "status": data.private_project2.issue_statuses.all()[0].pk,
+ "type": data.private_project2.issue_types.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 3,
+ "project": data.blocked_project.pk,
+ "severity": data.blocked_project.severities.all()[0].pk,
+ "priority": data.blocked_project.priorities.all()[0].pk,
+ "status": data.blocked_project.issue_statuses.all()[0].pk,
+ "type": data.blocked_project.issue_types.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_issue_put_update(client, data):
public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
@@ -171,32 +280,116 @@ def test_issue_update(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- issue_data = IssueSerializer(data.public_issue).data
- issue_data["subject"] = "test"
- issue_data = json.dumps(issue_data)
- results = helper_test_http_method(client, 'put', public_url, issue_data, users)
- assert results == [401, 403, 403, 200, 200]
+ issue_data = IssueSerializer(data.public_issue).data
+ issue_data["subject"] = "test"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', public_url, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
- issue_data = IssueSerializer(data.private_issue1).data
- issue_data["subject"] = "test"
- issue_data = json.dumps(issue_data)
- results = helper_test_http_method(client, 'put', private_url1, issue_data, users)
- assert results == [401, 403, 403, 200, 200]
+ issue_data = IssueSerializer(data.private_issue1).data
+ issue_data["subject"] = "test"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url1, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
- issue_data = IssueSerializer(data.private_issue2).data
- issue_data["subject"] = "test"
- issue_data = json.dumps(issue_data)
- results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
- assert results == [401, 403, 403, 200, 200]
+ issue_data = IssueSerializer(data.private_issue2).data
+ issue_data["subject"] = "test"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
- issue_data = IssueSerializer(data.blocked_issue).data
- issue_data["subject"] = "test"
- issue_data = json.dumps(issue_data)
- results = helper_test_http_method(client, 'put', blocked_url, issue_data, users)
- assert results == [401, 403, 403, 451, 451]
+ issue_data = IssueSerializer(data.blocked_issue).data
+ issue_data["subject"] = "test"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_data, users)
+ assert results == [401, 403, 403, 451, 451]
-def test_issue_update_with_project_change(client):
+def test_issue_put_comment(client, data):
+ public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ issue_data = IssueSerializer(data.public_issue).data
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', public_url, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.private_issue1).data
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url1, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.private_issue2).data
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.blocked_issue).data
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_issue_put_update_and_comment(client, data):
+ public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ issue_data = IssueSerializer(data.public_issue).data
+ issue_data["subject"] = "test"
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', public_url, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.private_issue1).data
+ issue_data["subject"] = "test"
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url1, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.private_issue2).data
+ issue_data["subject"] = "test"
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ issue_data = IssueSerializer(data.blocked_issue).data
+ issue_data["subject"] = "test"
+ issue_data["comment"] = "test comment"
+ issue_data = json.dumps(issue_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_issue_put_update_with_project_change(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
user3 = f.UserFactory.create()
@@ -309,139 +502,7 @@ def test_issue_update_with_project_change(client):
issue.save()
-def test_issue_delete(client, data):
- public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
- private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
- private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
- blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- ]
-
- results = helper_test_http_method(client, 'delete', public_url, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url1, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url2, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', blocked_url, None, users)
- assert results == [401, 403, 403, 451]
-
-
-def test_issue_list(client, data):
- url = reverse('issues-list')
-
- response = client.get(url)
- issues_data = json.loads(response.content.decode('utf-8'))
- assert len(issues_data) == 2
- assert response.status_code == 200
-
- client.login(data.registered_user)
-
- response = client.get(url)
- issues_data = json.loads(response.content.decode('utf-8'))
- assert len(issues_data) == 2
- assert response.status_code == 200
-
- client.login(data.project_member_with_perms)
-
- response = client.get(url)
- issues_data = json.loads(response.content.decode('utf-8'))
- assert len(issues_data) == 4
- assert response.status_code == 200
-
- client.login(data.project_owner)
-
- response = client.get(url)
- issues_data = json.loads(response.content.decode('utf-8'))
- assert len(issues_data) == 4
- assert response.status_code == 200
-
-
-def test_issue_list_filter_by_project_ok(client, data):
- url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk)
-
- client.login(data.project_owner)
- response = client.get(url)
-
- assert response.status_code == 200
- assert len(response.data) == 1
-
-
-def test_issue_list_filter_by_project_error(client, data):
- url = "{}?project={}".format(reverse("issues-list"), "-ERROR-")
-
- client.login(data.project_owner)
- response = client.get(url)
-
- assert response.status_code == 400
-
-
-def test_issue_create(client, data):
- url = reverse('issues-list')
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 1,
- "project": data.public_project.pk,
- "severity": data.public_project.severities.all()[0].pk,
- "priority": data.public_project.priorities.all()[0].pk,
- "status": data.public_project.issue_statuses.all()[0].pk,
- "type": data.public_project.issue_types.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 2,
- "project": data.private_project1.pk,
- "severity": data.private_project1.severities.all()[0].pk,
- "priority": data.private_project1.priorities.all()[0].pk,
- "status": data.private_project1.issue_statuses.all()[0].pk,
- "type": data.private_project1.issue_types.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 3,
- "project": data.private_project2.pk,
- "severity": data.private_project2.severities.all()[0].pk,
- "priority": data.private_project2.priorities.all()[0].pk,
- "status": data.private_project2.issue_statuses.all()[0].pk,
- "type": data.private_project2.issue_types.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 3,
- "project": data.blocked_project.pk,
- "severity": data.blocked_project.severities.all()[0].pk,
- "priority": data.blocked_project.priorities.all()[0].pk,
- "status": data.blocked_project.issue_statuses.all()[0].pk,
- "type": data.blocked_project.issue_types.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 451, 451]
-
-
-def test_issue_patch(client, data):
+def test_issue_patch_update(client, data):
public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
@@ -473,7 +534,110 @@ def test_issue_patch(client, data):
assert results == [401, 403, 403, 451, 451]
-def test_issue_bulk_create(client, data):
+def test_issue_patch_comment(client, data):
+ public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"comment": "test comment", "version": data.public_issue.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_issue1.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_issue2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.blocked_issue.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_issue_patch_update_and_comment(client, data):
+ public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.public_issue.version
+ })
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_issue1.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_issue2.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.blocked_issue.version
+ })
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_issue_delete(client, data):
+ public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk})
+ private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk})
+ private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ ]
+
+ results = helper_test_http_method(client, 'delete', public_url, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url1, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url2, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 451]
+
+
+def test_issue_action_bulk_create(client, data):
data.public_issue.project.default_issue_status = f.IssueStatusFactory()
data.public_issue.project.default_issue_type = f.IssueTypeFactory()
data.public_issue.project.default_priority = f.PriorityFactory()
@@ -633,34 +797,6 @@ def test_issue_voters_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_issues_csv(client, data):
- url = reverse('issues-csv')
- csv_public_uuid = data.public_project.issues_csv_uuid
- csv_private1_uuid = data.private_project1.issues_csv_uuid
- csv_private2_uuid = data.private_project2.issues_csv_uuid
- csv_blocked_uuid = data.blocked_project.issues_csv_uuid
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
-
def test_issue_action_watch(client, data):
public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk})
@@ -762,3 +898,31 @@ def test_issue_watchers_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'get', blocked_url, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_issues_csv(client, data):
+ url = reverse('issues-csv')
+ csv_public_uuid = data.public_project.issues_csv_uuid
+ csv_private1_uuid = data.private_project1.issues_csv_uuid
+ csv_private2_uuid = data.private_project2.issues_csv_uuid
+ csv_blocked_uuid = data.blocked_project.issues_csv_uuid
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index aff81389..63a1d63e 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -142,6 +142,36 @@ def data():
return m
+def test_task_list(client, data):
+ url = reverse('tasks-list')
+
+ response = client.get(url)
+ tasks_data = json.loads(response.content.decode('utf-8'))
+ assert len(tasks_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.registered_user)
+
+ response = client.get(url)
+ tasks_data = json.loads(response.content.decode('utf-8'))
+ assert len(tasks_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_member_with_perms)
+
+ response = client.get(url)
+ tasks_data = json.loads(response.content.decode('utf-8'))
+ assert len(tasks_data) == 4
+ assert response.status_code == 200
+
+ client.login(data.project_owner)
+
+ response = client.get(url)
+ tasks_data = json.loads(response.content.decode('utf-8'))
+ assert len(tasks_data) == 4
+ assert response.status_code == 200
+
+
def test_task_retrieve(client, data):
public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
@@ -166,7 +196,55 @@ def test_task_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_task_update(client, data):
+def test_task_create(client, data):
+ url = reverse('tasks-list')
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 1,
+ "project": data.public_project.pk,
+ "status": data.public_project.task_statuses.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 2,
+ "project": data.private_project1.pk,
+ "status": data.private_project1.task_statuses.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 3,
+ "project": data.private_project2.pk,
+ "status": data.private_project2.task_statuses.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "subject": "test",
+ "ref": 3,
+ "project": data.blocked_project.pk,
+ "status": data.blocked_project.task_statuses.all()[0].pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_task_put_update(client, data):
public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
@@ -181,32 +259,116 @@ def test_task_update(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- task_data = TaskSerializer(data.public_task).data
- task_data["subject"] = "test"
- task_data = json.dumps(task_data)
- results = helper_test_http_method(client, 'put', public_url, task_data, users)
- assert results == [401, 403, 403, 200, 200]
+ task_data = TaskSerializer(data.public_task).data
+ task_data["subject"] = "test"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', public_url, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
- task_data = TaskSerializer(data.private_task1).data
- task_data["subject"] = "test"
- task_data = json.dumps(task_data)
- results = helper_test_http_method(client, 'put', private_url1, task_data, users)
- assert results == [401, 403, 403, 200, 200]
+ task_data = TaskSerializer(data.private_task1).data
+ task_data["subject"] = "test"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url1, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
- task_data = TaskSerializer(data.private_task2).data
- task_data["subject"] = "test"
- task_data = json.dumps(task_data)
- results = helper_test_http_method(client, 'put', private_url2, task_data, users)
- assert results == [401, 403, 403, 200, 200]
+ task_data = TaskSerializer(data.private_task2).data
+ task_data["subject"] = "test"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url2, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
- task_data = TaskSerializer(data.blocked_task).data
- task_data["subject"] = "test"
- task_data = json.dumps(task_data)
- results = helper_test_http_method(client, 'put', blocked_url, task_data, users)
- assert results == [401, 403, 403, 451, 451]
+ task_data = TaskSerializer(data.blocked_task).data
+ task_data["subject"] = "test"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', blocked_url, task_data, users)
+ assert results == [401, 403, 403, 451, 451]
-def test_task_update_with_project_change(client):
+def test_task_put_comment(client, data):
+ public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ task_data = TaskSerializer(data.public_task).data
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', public_url, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.private_task1).data
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url1, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.private_task2).data
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url2, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.blocked_task).data
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', blocked_url, task_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_task_put_update_and_comment(client, data):
+ public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ task_data = TaskSerializer(data.public_task).data
+ task_data["subject"] = "test"
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', public_url, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.private_task1).data
+ task_data["subject"] = "test"
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url1, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.private_task2).data
+ task_data["subject"] = "test"
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', private_url2, task_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ task_data = TaskSerializer(data.blocked_task).data
+ task_data["subject"] = "test"
+ task_data["comment"] = "test comment"
+ task_data = json.dumps(task_data)
+ results = helper_test_http_method(client, 'put', blocked_url, task_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_task_put_update_with_project_change(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
user3 = f.UserFactory.create()
@@ -301,107 +463,7 @@ def test_task_update_with_project_change(client):
task.save()
-def test_task_delete(client, data):
- public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
- private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
- private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
- blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- ]
- results = helper_test_http_method(client, 'delete', public_url, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url1, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url2, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', blocked_url, None, users)
- assert results == [401, 403, 403, 451]
-
-
-def test_task_list(client, data):
- url = reverse('tasks-list')
-
- response = client.get(url)
- tasks_data = json.loads(response.content.decode('utf-8'))
- assert len(tasks_data) == 2
- assert response.status_code == 200
-
- client.login(data.registered_user)
-
- response = client.get(url)
- tasks_data = json.loads(response.content.decode('utf-8'))
- assert len(tasks_data) == 2
- assert response.status_code == 200
-
- client.login(data.project_member_with_perms)
-
- response = client.get(url)
- tasks_data = json.loads(response.content.decode('utf-8'))
- assert len(tasks_data) == 4
- assert response.status_code == 200
-
- client.login(data.project_owner)
-
- response = client.get(url)
- tasks_data = json.loads(response.content.decode('utf-8'))
- assert len(tasks_data) == 4
- assert response.status_code == 200
-
-
-def test_task_create(client, data):
- url = reverse('tasks-list')
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 1,
- "project": data.public_project.pk,
- "status": data.public_project.task_statuses.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 2,
- "project": data.private_project1.pk,
- "status": data.private_project1.task_statuses.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 3,
- "project": data.private_project2.pk,
- "status": data.private_project2.task_statuses.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "subject": "test",
- "ref": 3,
- "project": data.blocked_project.pk,
- "status": data.blocked_project.task_statuses.all()[0].pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 451, 451]
-
-
-def test_task_patch(client, data):
+def test_task_patch_update(client, data):
public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
@@ -433,6 +495,108 @@ def test_task_patch(client, data):
assert results == [401, 403, 403, 451, 451]
+def test_task_patch_comment(client, data):
+ public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"comment": "test comment", "version": data.public_task.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_task1.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_task2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.blocked_task.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_task_patch_update_and_comment(client, data):
+ public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.public_task.version
+ })
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_task1.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_task2.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.blocked_task.version
+ })
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_task_delete(client, data):
+ public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk})
+ private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk})
+ private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ ]
+ results = helper_test_http_method(client, 'delete', public_url, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url1, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url2, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 451]
+
+
def test_task_action_bulk_create(client, data):
url = reverse('tasks-bulk-create')
@@ -586,34 +750,6 @@ def test_task_voters_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_tasks_csv(client, data):
- url = reverse('tasks-csv')
- csv_public_uuid = data.public_project.tasks_csv_uuid
- csv_private1_uuid = data.private_project1.tasks_csv_uuid
- csv_private2_uuid = data.private_project1.tasks_csv_uuid
- csv_blocked_uuid = data.blocked_project.tasks_csv_uuid
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
-
def test_task_action_watch(client, data):
public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk})
@@ -716,3 +852,31 @@ def test_task_watchers_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'get', blocked_url, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_tasks_csv(client, data):
+ url = reverse('tasks-csv')
+ csv_public_uuid = data.public_project.tasks_csv_uuid
+ csv_private1_uuid = data.private_project1.tasks_csv_uuid
+ csv_private2_uuid = data.private_project1.tasks_csv_uuid
+ csv_blocked_uuid = data.blocked_project.tasks_csv_uuid
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 4da7b081..cb5f78ff 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -138,6 +138,36 @@ def data():
return m
+def test_user_story_list(client, data):
+ url = reverse('userstories-list')
+
+ response = client.get(url)
+ userstories_data = json.loads(response.content.decode('utf-8'))
+ assert len(userstories_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.registered_user)
+
+ response = client.get(url)
+ userstories_data = json.loads(response.content.decode('utf-8'))
+ assert len(userstories_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_member_with_perms)
+
+ response = client.get(url)
+ userstories_data = json.loads(response.content.decode('utf-8'))
+ assert len(userstories_data) == 4
+ assert response.status_code == 200
+
+ client.login(data.project_owner)
+
+ response = client.get(url)
+ userstories_data = json.loads(response.content.decode('utf-8'))
+ assert len(userstories_data) == 4
+ assert response.status_code == 200
+
+
def test_user_story_retrieve(client, data):
public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
@@ -162,7 +192,35 @@ def test_user_story_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_user_story_update(client, data):
+def test_user_story_create(client, data):
+ url = reverse('userstories-list')
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk})
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk})
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk})
+ results = helper_test_http_method(client, 'post', url, create_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_user_story_put_update(client, data):
public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
@@ -201,7 +259,92 @@ def test_user_story_update(client, data):
results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users)
assert results == [401, 403, 403, 451, 451]
-def test_user_story_update_with_project_change(client):
+
+def test_user_story_put_comment(client, data):
+ public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ user_story_data = UserStorySerializer(data.public_user_story).data
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', public_url, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.private_user_story1).data
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url1, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.private_user_story2).data
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.blocked_user_story).data
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_user_story_put_update_and_comment(client, data):
+ public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ user_story_data = UserStorySerializer(data.public_user_story).data
+ user_story_data["subject"] = "test"
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', public_url, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.private_user_story1).data
+ user_story_data["subject"] = "test"
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url1, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.private_user_story2).data
+ user_story_data["subject"] = "test"
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ user_story_data = UserStorySerializer(data.blocked_user_story).data
+ user_story_data["subject"] = "test"
+ user_story_data["comment"] = "test comment"
+ user_story_data = json.dumps(user_story_data)
+ results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_user_story_put_update_with_project_change(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
user3 = f.UserFactory.create()
@@ -296,87 +439,7 @@ def test_user_story_update_with_project_change(client):
us.save()
-def test_user_story_delete(client, data):
- public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
- private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
- private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
- blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- ]
- results = helper_test_http_method(client, 'delete', public_url, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url1, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url2, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', blocked_url, None, users)
- assert results == [401, 403, 403, 451]
-
-
-def test_user_story_list(client, data):
- url = reverse('userstories-list')
-
- response = client.get(url)
- userstories_data = json.loads(response.content.decode('utf-8'))
- assert len(userstories_data) == 2
- assert response.status_code == 200
-
- client.login(data.registered_user)
-
- response = client.get(url)
- userstories_data = json.loads(response.content.decode('utf-8'))
- assert len(userstories_data) == 2
- assert response.status_code == 200
-
- client.login(data.project_member_with_perms)
-
- response = client.get(url)
- userstories_data = json.loads(response.content.decode('utf-8'))
- assert len(userstories_data) == 4
- assert response.status_code == 200
-
- client.login(data.project_owner)
-
- response = client.get(url)
- userstories_data = json.loads(response.content.decode('utf-8'))
- assert len(userstories_data) == 4
- assert response.status_code == 200
-
-
-def test_user_story_create(client, data):
- url = reverse('userstories-list')
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk})
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk})
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk})
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk})
- results = helper_test_http_method(client, 'post', url, create_data, users)
- assert results == [401, 403, 403, 451, 451]
-
-
-def test_user_story_patch(client, data):
+def test_user_story_patch_update(client, data):
public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
@@ -408,6 +471,109 @@ def test_user_story_patch(client, data):
assert results == [401, 403, 403, 451, 451]
+def test_user_story_patch_comment(client, data):
+ public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"comment": "test comment", "version": data.public_user_story.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story1.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.blocked_user_story.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_user_story_patch_update_and_comment(client, data):
+ public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.public_user_story.version
+ })
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_user_story1.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.private_user_story2.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "subject": "test",
+ "comment": "test comment",
+ "version": data.blocked_user_story.version
+ })
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_user_story_delete(client, data):
+ public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk})
+ private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk})
+ private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ ]
+ results = helper_test_http_method(client, 'delete', public_url, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url1, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url2, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 451]
+
+
+
def test_user_story_action_bulk_create(client, data):
url = reverse('userstories-bulk-create')
@@ -580,30 +746,6 @@ def test_user_story_voters_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
-def test_user_stories_csv(client, data):
- url = reverse('userstories-csv')
- csv_public_uuid = data.public_project.userstories_csv_uuid
- csv_private1_uuid = data.private_project1.userstories_csv_uuid
- csv_private2_uuid = data.private_project1.userstories_csv_uuid
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
- results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
- assert results == [200, 200, 200, 200, 200]
-
-
def test_user_story_action_watch(client, data):
public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk})
@@ -706,3 +848,27 @@ def test_userstory_watchers_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'get', blocked_url, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+def test_user_stories_action_csv(client, data):
+ url = reverse('userstories-csv')
+ csv_public_uuid = data.public_project.userstories_csv_uuid
+ csv_private1_uuid = data.private_project1.userstories_csv_uuid
+ csv_private2_uuid = data.private_project1.userstories_csv_uuid
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
+
+ results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
+ assert results == [200, 200, 200, 200, 200]
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index aaee4e53..e981aa75 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -111,91 +111,9 @@ def data():
return m
-def test_wiki_page_retrieve(client, data):
- public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
- private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
- private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
- blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- results = helper_test_http_method(client, 'get', public_url, None, users)
- assert results == [200, 200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private_url1, None, users)
- assert results == [200, 200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private_url2, None, users)
- assert results == [401, 403, 403, 200, 200]
- results = helper_test_http_method(client, 'get', blocked_url, None, users)
- assert results == [401, 403, 403, 200, 200]
-
-
-def test_wiki_page_update(client, data):
- public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
- private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
- private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
- blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- wiki_page_data = WikiPageSerializer(data.public_wiki_page).data
- wiki_page_data["content"] = "test"
- wiki_page_data = json.dumps(wiki_page_data)
- results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data
- wiki_page_data["content"] = "test"
- wiki_page_data = json.dumps(wiki_page_data)
- results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data
- wiki_page_data["content"] = "test"
- wiki_page_data = json.dumps(wiki_page_data)
- results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data
- wiki_page_data["content"] = "test"
- wiki_page_data = json.dumps(wiki_page_data)
- results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users)
- assert results == [401, 403, 403, 451, 451]
-
-
-def test_wiki_page_delete(client, data):
- public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
- private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
- private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
- blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- ]
- results = helper_test_http_method(client, 'delete', public_url, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url1, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', private_url2, None, users)
- assert results == [401, 403, 403, 204]
- results = helper_test_http_method(client, 'delete', blocked_url, None, users)
- assert results == [401, 403, 403, 451]
-
+##############################################
+## WIKI PAGES
+##############################################
def test_wiki_page_list(client, data):
url = reverse('wiki-list')
@@ -227,6 +145,30 @@ def test_wiki_page_list(client, data):
assert response.status_code == 200
+def test_wiki_page_retrieve(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
def test_wiki_page_create(client, data):
url = reverse('wiki-list')
@@ -270,7 +212,8 @@ def test_wiki_page_create(client, data):
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete())
assert results == [401, 403, 403, 451, 451]
-def test_wiki_page_patch(client, data):
+
+def test_wiki_page_put_update(client, data):
public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
@@ -285,68 +228,36 @@ def test_wiki_page_patch(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version})
- results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.public_wiki_page).data
+ wiki_page_data["content"] = "test"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
- results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data
+ wiki_page_data["content"] = "test"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
- results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data
+ wiki_page_data["content"] = "test"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version})
- results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
- assert results == [401, 403, 403, 451, 451]
+ wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data
+ wiki_page_data["content"] = "test"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 451, 451]
-def test_wiki_page_action_render(client, data):
- url = reverse('wiki-render')
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- post_data = json.dumps({"content": "test", "project_id": data.public_project.pk})
- results = helper_test_http_method(client, 'post', url, post_data, users)
- assert results == [200, 200, 200, 200, 200]
-
-
-def test_wiki_link_retrieve(client, data):
- public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
- private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
- private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
- blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- results = helper_test_http_method(client, 'get', public_url, None, users)
- assert results == [200, 200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private_url1, None, users)
- assert results == [200, 200, 200, 200, 200]
- results = helper_test_http_method(client, 'get', private_url2, None, users)
- assert results == [401, 403, 403, 200, 200]
- results = helper_test_http_method(client, 'get', blocked_url, None, users)
- assert results == [401, 403, 403, 200, 200]
-
-
-def test_wiki_link_update(client, data):
- public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
- private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
- private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
- blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+def test_wiki_page_put_comment(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
users = [
None,
@@ -357,35 +268,278 @@ def test_wiki_link_update(client, data):
]
with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data
- wiki_link_data["title"] = "test"
- wiki_link_data = json.dumps(wiki_link_data)
- results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.public_wiki_page).data
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data
- wiki_link_data["title"] = "test"
- wiki_link_data = json.dumps(wiki_link_data)
- results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data
- wiki_link_data["title"] = "test"
- wiki_link_data = json.dumps(wiki_link_data)
- results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users)
- assert results == [401, 403, 403, 200, 200]
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
- wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data
- wiki_link_data["title"] = "test"
- wiki_link_data = json.dumps(wiki_link_data)
- results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users)
- assert results == [401, 403, 403, 451, 451]
+ wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 451, 451]
-def test_wiki_link_delete(client, data):
- public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
- private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
- private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
- blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+
+def test_wiki_page_put_update_and_comment(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ wiki_page_data = WikiPageSerializer(data.public_wiki_page).data
+ wiki_page_data["slug"] = "test"
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data
+ wiki_page_data["slug"] = "test"
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data
+ wiki_page_data["slug"] = "test"
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data
+ wiki_page_data["slug"] = "test"
+ wiki_page_data["comment"] = "test comment"
+ wiki_page_data = json.dumps(wiki_page_data)
+ results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_page_put_update_with_project_change(client):
+ user1 = f.UserFactory.create()
+ user2 = f.UserFactory.create()
+ user3 = f.UserFactory.create()
+ user4 = f.UserFactory.create()
+ project1 = f.ProjectFactory()
+ project2 = f.ProjectFactory()
+
+ membership1 = f.MembershipFactory(project=project1,
+ user=user1,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ membership2 = f.MembershipFactory(project=project2,
+ user=user1,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ membership3 = f.MembershipFactory(project=project1,
+ user=user2,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ membership4 = f.MembershipFactory(project=project2,
+ user=user3,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+
+ wiki_page = f.WikiPageFactory.create(project=project1)
+
+ url = reverse('wiki-detail', kwargs={"pk": wiki_page.pk})
+
+ # Test user with permissions in both projects
+ client.login(user1)
+
+ wiki_page_data = WikiPageSerializer(wiki_page).data
+ wiki_page_data["project"] = project2.id
+ wiki_page_data = json.dumps(wiki_page_data)
+
+ response = client.put(url, data=wiki_page_data, content_type="application/json")
+
+ assert response.status_code == 200
+
+ wiki_page.project = project1
+ wiki_page.save()
+
+ # Test user with permissions in only origin project
+ client.login(user2)
+
+ wiki_page_data = WikiPageSerializer(wiki_page).data
+ wiki_page_data["project"] = project2.id
+ wiki_page_data = json.dumps(wiki_page_data)
+
+ response = client.put(url, data=wiki_page_data, content_type="application/json")
+
+ assert response.status_code == 403
+
+ wiki_page.project = project1
+ wiki_page.save()
+
+ # Test user with permissions in only destionation project
+ client.login(user3)
+
+ wiki_page_data = WikiPageSerializer(wiki_page).data
+ wiki_page_data["project"] = project2.id
+ wiki_page_data = json.dumps(wiki_page_data)
+
+ response = client.put(url, data=wiki_page_data, content_type="application/json")
+
+ assert response.status_code == 403
+
+ wiki_page.project = project1
+ wiki_page.save()
+
+ # Test user without permissions in the projects
+ client.login(user4)
+
+ wiki_page_data = WikiPageSerializer(wiki_page).data
+ wiki_page_data["project"] = project2.id
+ wiki_page_data = json.dumps(wiki_page_data)
+
+ response = client.put(url, data=wiki_page_data, content_type="application/json")
+
+ assert response.status_code == 403
+
+ wiki_page.project = project1
+ wiki_page.save()
+
+
+def test_wiki_page_patch_update(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_page_patch_comment(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"comment": "test comment", "version": data.public_wiki_page.version})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"comment": "test comment", "version": data.blocked_wiki_page.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_page_patch_update_and_comment(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({
+ "content": "test",
+ "comment": "test comment",
+ "version": data.public_wiki_page.version
+ })
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "content": "test",
+ "comment": "test comment",
+ "version": data.private_wiki_page2.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "content": "test",
+ "comment": "test comment",
+ "version": data.private_wiki_page2.version
+ })
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({
+ "content": "test",
+ "comment": "test comment",
+ "version": data.blocked_wiki_page.version
+ })
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_page_delete(client, data):
+ public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk})
+ private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk})
+ private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk})
users = [
None,
@@ -403,38 +557,8 @@ def test_wiki_link_delete(client, data):
assert results == [401, 403, 403, 451]
-def test_wiki_link_list(client, data):
- url = reverse('wiki-links-list')
-
- response = client.get(url)
- wiki_links_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_links_data) == 2
- assert response.status_code == 200
-
- client.login(data.registered_user)
-
- response = client.get(url)
- wiki_links_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_links_data) == 2
- assert response.status_code == 200
-
- client.login(data.project_member_with_perms)
-
- response = client.get(url)
- wiki_links_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_links_data) == 4
- assert response.status_code == 200
-
- client.login(data.project_owner)
-
- response = client.get(url)
- wiki_links_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_links_data) == 4
- assert response.status_code == 200
-
-
-def test_wiki_link_create(client, data):
- url = reverse('wiki-links-list')
+def test_wiki_page_action_render(client, data):
+ url = reverse('wiki-render')
users = [
None,
@@ -444,69 +568,9 @@ def test_wiki_link_create(client, data):
data.project_owner
]
- create_data = json.dumps({
- "title": "test",
- "href": "test",
- "project": data.public_project.pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "title": "test",
- "href": "test",
- "project": data.private_project1.pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "title": "test",
- "href": "test",
- "project": data.private_project2.pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 403, 403, 201, 201]
-
- create_data = json.dumps({
- "title": "test",
- "href": "test",
- "project": data.blocked_project.pk,
- })
- results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
- assert results == [401, 403, 403, 451, 451]
-
-
-def test_wiki_link_patch(client, data):
- public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
- private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
- private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
- blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
-
- users = [
- None,
- data.registered_user,
- data.project_member_without_perms,
- data.project_member_with_perms,
- data.project_owner
- ]
-
- with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
- assert results == [401, 403, 403, 200, 200]
-
- patch_data = json.dumps({"title": "test"})
- results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
- assert results == [401, 403, 403, 451, 451]
+ post_data = json.dumps({"content": "test", "project_id": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [200, 200, 200, 200, 200]
def test_wikipage_action_watch(client, data):
@@ -610,3 +674,199 @@ def test_wikipage_watchers_retrieve(client, data):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'get', blocked_url, None, users)
assert results == [401, 403, 403, 200, 200]
+
+
+##############################################
+## WIKI LINKS
+##############################################
+
+def test_wiki_link_list(client, data):
+ url = reverse('wiki-links-list')
+
+ response = client.get(url)
+ wiki_links_data = json.loads(response.content.decode('utf-8'))
+ assert len(wiki_links_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.registered_user)
+
+ response = client.get(url)
+ wiki_links_data = json.loads(response.content.decode('utf-8'))
+ assert len(wiki_links_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_member_with_perms)
+
+ response = client.get(url)
+ wiki_links_data = json.loads(response.content.decode('utf-8'))
+ assert len(wiki_links_data) == 4
+ assert response.status_code == 200
+
+ client.login(data.project_owner)
+
+ response = client.get(url)
+ wiki_links_data = json.loads(response.content.decode('utf-8'))
+ assert len(wiki_links_data) == 4
+ assert response.status_code == 200
+
+
+def test_wiki_link_retrieve(client, data):
+ public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
+ private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
+ private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
+ blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ results = helper_test_http_method(client, 'get', public_url, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [200, 200, 200, 200, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
+
+
+def test_wiki_link_create(client, data):
+ url = reverse('wiki-links-list')
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ create_data = json.dumps({
+ "title": "test",
+ "href": "test",
+ "project": data.public_project.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "title": "test",
+ "href": "test",
+ "project": data.private_project1.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "title": "test",
+ "href": "test",
+ "project": data.private_project2.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
+ assert results == [401, 403, 403, 201, 201]
+
+ create_data = json.dumps({
+ "title": "test",
+ "href": "test",
+ "project": data.blocked_project.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete())
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_link_update(client, data):
+ public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
+ private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
+ private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
+ blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data
+ wiki_link_data["title"] = "test"
+ wiki_link_data = json.dumps(wiki_link_data)
+ results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data
+ wiki_link_data["title"] = "test"
+ wiki_link_data = json.dumps(wiki_link_data)
+ results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data
+ wiki_link_data["title"] = "test"
+ wiki_link_data = json.dumps(wiki_link_data)
+ results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data
+ wiki_link_data["title"] = "test"
+ wiki_link_data = json.dumps(wiki_link_data)
+ results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_link_patch(client, data):
+ public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
+ private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
+ private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
+ blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"):
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [401, 403, 403, 200, 200]
+
+ patch_data = json.dumps({"title": "test"})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
+def test_wiki_link_delete(client, data):
+ public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk})
+ private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk})
+ private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk})
+ blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk})
+
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ ]
+ results = helper_test_http_method(client, 'delete', public_url, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url1, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', private_url2, None, users)
+ assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 451]
From b2724723963fede753a8a1abfbf464fa15e0a2f8 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 24 May 2016 09:10:22 +0200
Subject: [PATCH 024/261] Adding endpoint to bulk updating the milestone for
user stories
---
taiga/projects/userstories/api.py | 122 +++++++++++++---------
taiga/projects/userstories/permissions.py | 1 +
taiga/projects/userstories/serializers.py | 33 +++++-
taiga/projects/userstories/services.py | 15 +++
tests/integration/test_userstories.py | 68 ++++++++++++
5 files changed, 185 insertions(+), 54 deletions(-)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 745268cd..1b0b8035 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -17,7 +17,6 @@
from contextlib import suppress
-
from django.apps import apps
from django.db import transaction
from django.utils.translation import ugettext as _
@@ -37,6 +36,7 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersVi
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.models import Project, UserStoryStatus
+from taiga.projects.milestones.models import Milestone
from taiga.projects.history.services import take_snapshot
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
@@ -86,32 +86,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return serializers.UserStorySerializer
- def update(self, request, *args, **kwargs):
- self.object = self.get_object_or_none()
- project_id = request.DATA.get('project', None)
- if project_id and self.object and self.object.project.id != project_id:
- try:
- new_project = Project.objects.get(pk=project_id)
- self.check_permissions(request, "destroy", self.object)
- self.check_permissions(request, "create", new_project)
-
- sprint_id = request.DATA.get('milestone', None)
- if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0:
- request.DATA['milestone'] = None
-
- status_id = request.DATA.get('status', None)
- if status_id is not None:
- try:
- old_status = self.object.project.us_statuses.get(pk=status_id)
- new_status = new_project.us_statuses.get(slug=old_status.slug)
- request.DATA['status'] = new_status.id
- except UserStoryStatus.DoesNotExist:
- request.DATA['status'] = new_project.default_us_status.id
- except Project.DoesNotExist:
- return response.BadRequest(_("The project doesn't exist"))
-
- return super().update(request, *args, **kwargs)
-
def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related("role_points",
@@ -126,6 +100,17 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
+ def pre_conditions_on_save(self, obj):
+ super().pre_conditions_on_save(obj)
+
+ if obj.milestone and obj.milestone.project != obj.project:
+ raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
+ "to this user story."))
+
+ if obj.status and obj.status.project != obj.project:
+ raise exc.PermissionDenied(_("You don't have permissions to set this status "
+ "to this user story."))
+
def pre_save(self, obj):
# This is very ugly hack, but having
# restframework is the only way to do it.
@@ -155,16 +140,49 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
super().post_save(obj, created)
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
+ @transaction.atomic
+ def create(self, *args, **kwargs):
+ response = super().create(*args, **kwargs)
- if obj.milestone and obj.milestone.project != obj.project:
- raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
- "to this user story."))
+ # Added comment to the origin (issue)
+ if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue:
+ self.object.generated_from_issue.save()
- if obj.status and obj.status.project != obj.project:
- raise exc.PermissionDenied(_("You don't have permissions to set this status "
- "to this user story."))
+ comment = _("Generating the user story #{ref} - {subject}")
+ comment = comment.format(ref=self.object.ref, subject=self.object.subject)
+ history = take_snapshot(self.object.generated_from_issue,
+ comment=comment,
+ user=self.request.user)
+
+ self.send_notifications(self.object.generated_from_issue, history)
+
+ return response
+
+ def update(self, request, *args, **kwargs):
+ self.object = self.get_object_or_none()
+ project_id = request.DATA.get('project', None)
+ if project_id and self.object and self.object.project.id != project_id:
+ try:
+ new_project = Project.objects.get(pk=project_id)
+ self.check_permissions(request, "destroy", self.object)
+ self.check_permissions(request, "create", new_project)
+
+ sprint_id = request.DATA.get('milestone', None)
+ if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0:
+ request.DATA['milestone'] = None
+
+ status_id = request.DATA.get('status', None)
+ if status_id is not None:
+ try:
+ old_status = self.object.project.us_statuses.get(pk=status_id)
+ new_status = new_project.us_statuses.get(slug=old_status.slug)
+ request.DATA['status'] = new_status.id
+ except UserStoryStatus.DoesNotExist:
+ request.DATA['status'] = new_project.default_us_status.id
+ except Project.DoesNotExist:
+ return response.BadRequest(_("The project doesn't exist"))
+
+ return super().update(request, *args, **kwargs)
@list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs):
@@ -224,6 +242,23 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return response.Ok(user_stories_serialized.data)
return response.BadRequest(serializer.errors)
+ @list_route(methods=["POST"])
+ def bulk_update_milestone(self, request, **kwargs):
+ serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA)
+ if not serializer.is_valid():
+ return response.BadRequest(serializer.errors)
+
+ data = serializer.data
+ project = get_object_or_404(Project, pk=data["project_id"])
+ milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
+
+ self.check_permissions(request, "bulk_update_milestone", project)
+
+ services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone)
+ services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
+
+ return response.NoContent()
+
def _bulk_update_order(self, order_field, request, **kwargs):
serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA)
if not serializer.is_valid():
@@ -255,23 +290,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
def bulk_update_kanban_order(self, request, **kwargs):
return self._bulk_update_order("kanban_order", request, **kwargs)
- @transaction.atomic
- def create(self, *args, **kwargs):
- response = super().create(*args, **kwargs)
-
- # Added comment to the origin (issue)
- if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue:
- self.object.generated_from_issue.save()
-
- comment = _("Generating the user story #{ref} - {subject}")
- comment = comment.format(ref=self.object.ref, subject=self.object.subject)
- history = take_snapshot(self.object.generated_from_issue,
- comment=comment,
- user=self.request.user)
-
- self.send_notifications(self.object.generated_from_issue, history)
-
- return response
class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.UserStoryVotersPermission,)
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index c91ef2a7..51c6f01a 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -34,6 +34,7 @@ class UserStoryPermission(TaigaResourcePermission):
csv_perms = AllowAny()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_us')
+ bulk_update_milestone_perms = HasProjectPerm('modify_us')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index 0931cae8..0a856e8a 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -17,6 +17,7 @@
from django.apps import apps
from taiga.base.api import serializers
+from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import TagsField
from taiga.base.fields import PickledObjectField
from taiga.base.fields import PgArrayField
@@ -24,8 +25,9 @@ from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender
-from taiga.projects.validators import ProjectExistsValidator
-from taiga.projects.validators import UserStoryStatusExistsValidator
+from taiga.projects.models import Project
+from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
+from taiga.projects.milestones.validators import SprintExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer
@@ -142,3 +144,30 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
+
+
+## Milestone bulk serializers
+
+class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
+ us_id = serializers.IntegerField()
+
+
+class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer):
+ project_id = serializers.IntegerField()
+ milestone_id = serializers.IntegerField()
+ bulk_stories = _UserStoryMilestoneBulkSerializer(many=True)
+
+ def validate(self, data):
+ """
+ All the userstories and the milestone are from the same project
+ """
+ user_story_ids = [us["us_id"] for us in data["bulk_stories"]]
+ project = get_object_or_404(Project, pk=data["project_id"])
+
+ if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids):
+ raise serializers.ValidationError("all the user stories must be from the same project")
+
+ if project.milestones.filter(id=data["milestone_id"]).count() != 1:
+ raise serializers.ValidationError("the milestone isn't valid for the project")
+
+ return data
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index b0b881a6..c1884228 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -91,6 +91,21 @@ def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object):
db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory)
+def update_userstories_milestone_in_bulk(bulk_data:list, milestone:object):
+ """
+ Update the milestone of some user stories.
+ `bulk_data` should be a list of user story ids:
+ """
+ user_story_ids = [us_data["us_id"] for us_data in bulk_data]
+ new_milestone_values = [{"milestone": milestone.id}] * len(user_story_ids)
+
+ events.emit_event_for_ids(ids=user_story_ids,
+ content_type="userstories.userstory",
+ projectid=milestone.project.pk)
+
+ db.update_in_bulk_with_ids(user_story_ids, new_milestone_values, model=models.UserStory)
+
+
def snapshot_userstories_in_bulk(bulk_data, user):
user_story_ids = []
for us_data in bulk_data:
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 8081eefd..bcb0a618 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -164,6 +164,74 @@ def test_api_update_orders_in_bulk(client):
assert response3.status_code == 204, response3.data
+def test_api_update_milestone_in_bulk(client):
+ project = f.create_project()
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ us1 = f.create_userstory(project=project)
+ us2 = f.create_userstory(project=project)
+ milestone = f.MilestoneFactory.create(project=project)
+
+ url = reverse("userstories-bulk-update-milestone")
+ data = {
+ "project_id": project.id,
+ "milestone_id": milestone.id,
+ "bulk_stories": [{"us_id": us1.id},
+ {"us_id": us2.id}]
+ }
+
+ client.login(project.owner)
+
+ assert project.milestones.get(id=milestone.id).user_stories.count() == 0
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 204, response.data
+ assert project.milestones.get(id=milestone.id).user_stories.count() == 2
+
+
+def test_api_update_milestone_in_bulk_invalid_milestone(client):
+ project = f.create_project()
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ us1 = f.create_userstory(project=project)
+ us2 = f.create_userstory(project=project)
+ m1 = f.MilestoneFactory.create(project=project)
+ m2 = f.MilestoneFactory.create()
+
+ url = reverse("userstories-bulk-update-milestone")
+ data = {
+ "project_id": project.id,
+ "milestone_id": m2.id,
+ "bulk_stories": [{"us_id": us1.id},
+ {"us_id": us2.id}]
+ }
+
+ client.login(project.owner)
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert response.data["non_field_errors"][0] == "the milestone isn't valid for the project"
+
+
+def test_api_update_milestone_in_bulk_invalid_userstories(client):
+ project = f.create_project()
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
+ us1 = f.create_userstory(project=project)
+ us2 = f.create_userstory()
+ milestone = f.MilestoneFactory.create(project=project)
+
+ url = reverse("userstories-bulk-update-milestone")
+ data = {
+ "project_id": project.id,
+ "milestone_id": milestone.id,
+ "bulk_stories": [{"us_id": us1.id},
+ {"us_id": us2.id}]
+ }
+
+ client.login(project.owner)
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert response.data["non_field_errors"][0] == "all the user stories must be from the same project"
+
+
def test_update_userstory_points(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
From 1556d45ae6d6ffd0f3d47b8ede3970a5233e7aa1 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 26 May 2016 14:43:33 +0200
Subject: [PATCH 025/261] Generating application token only when it doesn't
exist
---
taiga/external_apps/models.py | 3 ++-
tests/integration/test_application_tokens.py | 25 ++++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/taiga/external_apps/models.py b/taiga/external_apps/models.py
index fcc4695c..979978f4 100644
--- a/taiga/external_apps/models.py
+++ b/taiga/external_apps/models.py
@@ -83,4 +83,5 @@ class ApplicationToken(models.Model):
def generate_token(self):
self.auth_code = None
- self.token = _generate_uuid()
+ if not self.token:
+ self.token = _generate_uuid()
diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py
index da6d986e..da381179 100644
--- a/tests/integration/test_application_tokens.py
+++ b/tests/integration/test_application_tokens.py
@@ -100,3 +100,28 @@ def test_token_validate(client):
decyphered_token = encryption.decrypt(response.data["cyphered_token"], token.application.key)[0]
decyphered_token = json.loads(decyphered_token.decode("utf-8"))
assert decyphered_token["token"] == token.token
+
+
+def test_token_validate_validated(client):
+ # Validating a validated token should update the token attribute
+ user = f.UserFactory.create()
+ application = f.ApplicationFactory(next_url="http://next.url")
+ token = f.ApplicationTokenFactory(
+ auth_code="test-auth-code",
+ state="test-state",
+ application=application,
+ token="existing-token")
+
+ url = reverse("application-tokens-validate")
+ client.login(user)
+
+ data = {
+ "application": token.application.id,
+ "auth_code": "test-auth-code",
+ "state": "test-state"
+ }
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+
+ token = models.ApplicationToken.objects.get(id=token.id)
+ assert token.token == "existing-token"
From 532d633467eff04bc61cc7bc9ab58fa2be2844fb Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 30 May 2016 12:18:29 +0200
Subject: [PATCH 026/261] Improving-project-deletion
---
taiga/projects/api.py | 11 ++++--
.../migrations/0043_auto_20160530_1004.py | 22 ++++++++++++
taiga/projects/models.py | 2 +-
taiga/projects/services/__init__.py | 2 ++
taiga/projects/services/projects.py | 21 ++++++++++-
tests/integration/test_projects.py | 35 +++++++++++++++++++
6 files changed, 88 insertions(+), 5 deletions(-)
create mode 100644 taiga/projects/migrations/0043_auto_20160530_1004.py
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 17684fd3..b37250a5 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -20,6 +20,7 @@ from easy_thumbnails.source_generators import pil_image
from dateutil.relativedelta import relativedelta
from django.apps import apps
+from django.conf import settings
from django.db.models import signals, Prefetch
from django.db.models import Value as V
from django.db.models.functions import Coalesce
@@ -442,9 +443,13 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.pre_delete(obj)
self.pre_conditions_on_delete(obj)
- obj.delete_related_content()
- obj.delete()
- self.post_delete(obj)
+
+ services.orphan_project(obj)
+ if settings.CELERY_ENABLED:
+ services.delete_project.delay(obj.id)
+ else:
+ services.delete_project(obj.id)
+
return response.NoContent()
diff --git a/taiga/projects/migrations/0043_auto_20160530_1004.py b/taiga/projects/migrations/0043_auto_20160530_1004.py
new file mode 100644
index 00000000..5ad8f1ad
--- /dev/null
+++ b/taiga/projects/migrations/0043_auto_20160530_1004.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-30 10:04
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0042_auto_20160525_0911'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='owner',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_projects', to=settings.AUTH_USER_MODEL, verbose_name='owner'),
+ ),
+ ]
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 506c2736..4ff459e0 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -157,7 +157,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
default=timezone.now)
modified_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("modified date"))
- owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
+ owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name="owned_projects", verbose_name=_("owner"))
members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects",
through="Membership", verbose_name=_("members"),
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index 23d6334c..d33d51e9 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -47,6 +47,8 @@ from .projects import check_if_project_privacity_can_be_changed
from .projects import check_if_project_can_be_created_or_updated
from .projects import check_if_project_can_be_transfered
from .projects import check_if_project_is_out_of_owner_limits
+from .projects import orphan_project
+from .projects import delete_project
from .stats import get_stats_for_project_issues
from .stats import get_stats_for_project
diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py
index 1ac6d2b7..43309851 100644
--- a/taiga/projects/services/projects.py
+++ b/taiga/projects/services/projects.py
@@ -15,8 +15,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.apps import apps
from django.utils.translation import ugettext as _
-
+from taiga.celery import app
ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships'
ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships'
@@ -151,3 +152,21 @@ def check_if_project_is_out_of_owner_limits(project):
return True
return False
+
+
+def orphan_project(project):
+ project.memberships.filter(user=project.owner).delete()
+ project.owner = None
+ project.save()
+
+
+@app.task
+def delete_project(project_id):
+ Project = apps.get_model("projects", "Project")
+ try:
+ project = Project.objects.get(id=project_id)
+ except Project.DoesNotExist:
+ return
+
+ project.delete_related_content()
+ project.delete()
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 28a0dbe3..fbc09722 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -19,6 +19,8 @@ from easy_thumbnails.files import generate_all_aliases, get_thumbnailer
import os.path
import pytest
+from unittest import mock
+
pytestmark = pytest.mark.django_db
class ExpiredSigner(signing.TimestampSigner):
@@ -1814,3 +1816,36 @@ def test_public_project_when_project_has_unlimited_members(client):
project.owner.max_memberships_public_projects = None
assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_delete_project_with_celery_enabled(client, settings):
+ settings.CELERY_ENABLED = True
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-detail", args=(project.id,))
+ client.login(user)
+
+ #delete_project task should have been launched
+ with mock.patch('taiga.projects.services.delete_project') as delete_project_mock:
+ response = client.json.delete(url)
+ assert response.status_code == 204
+ project = Project.objects.get(id=project.id)
+ assert project.owner == None
+ assert project.memberships.count() == 0
+ delete_project_mock.delay.assert_called_once_with(project.id)
+
+
+def test_delete_project_with_celery_disabled(client, settings):
+ settings.CELERY_ENABLED = False
+
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-detail", args=(project.id,))
+ client.login(user)
+ response = client.json.delete(url)
+ assert response.status_code == 204
+ assert Project.objects.filter(id=project.id).count() == 0
From 3fd2d1cade716f264b2febc3627b1443a1d3e604 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 30 May 2016 18:31:56 +0200
Subject: [PATCH 027/261] Fix a problem with a migration between master and
stable branch
---
taiga/projects/migrations/0043_auto_20160530_1004.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/migrations/0043_auto_20160530_1004.py b/taiga/projects/migrations/0043_auto_20160530_1004.py
index 5ad8f1ad..101b3b6e 100644
--- a/taiga/projects/migrations/0043_auto_20160530_1004.py
+++ b/taiga/projects/migrations/0043_auto_20160530_1004.py
@@ -10,7 +10,7 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('projects', '0042_auto_20160525_0911'),
+ ('projects', '0040_remove_memberships_of_cancelled_users_acounts'),
]
operations = [
From 3c15b0ab1a7b3b8dd3df124bd687c024e8ee28a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 30 May 2016 18:39:10 +0200
Subject: [PATCH 028/261] Create a merge migration to fix the problem between
master and stable branches
---
taiga/projects/migrations/0044_merge.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 taiga/projects/migrations/0044_merge.py
diff --git a/taiga/projects/migrations/0044_merge.py b/taiga/projects/migrations/0044_merge.py
new file mode 100644
index 00000000..6bf0227c
--- /dev/null
+++ b/taiga/projects/migrations/0044_merge.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-30 16:36
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0043_auto_20160530_1004'),
+ ('projects', '0042_auto_20160525_0911'),
+ ]
+
+ operations = [
+ ]
From 47907eedb4b044599ad080d96587c76852a61d9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 30 May 2016 20:11:13 +0200
Subject: [PATCH 029/261] Apply PEP-263 to taiga project
---
manage.py | 19 +++++++++++++++++++
settings/__init__.py | 1 +
settings/celery.py | 1 +
settings/common.py | 1 +
settings/development.py | 1 +
settings/local.py.example | 1 +
settings/sr.py | 1 +
settings/testing.py | 1 +
settings/travis.py | 1 +
taiga/__init__.py | 1 +
taiga/auth/__init__.py | 1 -
taiga/auth/api.py | 1 +
taiga/auth/backends.py | 1 +
taiga/auth/permissions.py | 1 +
taiga/auth/serializers.py | 1 +
taiga/auth/services.py | 1 +
taiga/auth/signals.py | 1 +
taiga/auth/tokens.py | 1 +
taiga/base/__init__.py | 1 +
taiga/base/api/__init__.py | 1 +
taiga/base/api/authentication.py | 1 +
taiga/base/api/fields.py | 1 +
taiga/base/api/generics.py | 1 +
taiga/base/api/mixins.py | 1 +
taiga/base/api/negotiation.py | 1 +
taiga/base/api/pagination.py | 1 +
taiga/base/api/parsers.py | 1 +
taiga/base/api/permissions.py | 1 +
taiga/base/api/relations.py | 1 +
taiga/base/api/renderers.py | 1 +
taiga/base/api/request.py | 1 +
taiga/base/api/reverse.py | 1 +
taiga/base/api/serializers.py | 1 +
taiga/base/api/settings.py | 1 +
taiga/base/api/templatetags/api.py | 1 +
taiga/base/api/throttling.py | 1 +
taiga/base/api/urlpatterns.py | 1 +
taiga/base/api/urls.py | 1 +
taiga/base/api/utils.py | 1 +
taiga/base/api/utils/__init__.py | 1 +
taiga/base/api/utils/breadcrumbs.py | 1 +
taiga/base/api/utils/encoders.py | 1 +
taiga/base/api/utils/formatting.py | 1 +
taiga/base/api/utils/mediatypes.py | 1 +
taiga/base/api/views.py | 1 +
taiga/base/api/viewsets.py | 1 +
taiga/base/apps.py | 1 +
taiga/base/connectors/exceptions.py | 1 +
taiga/base/decorators.py | 1 +
taiga/base/exceptions.py | 1 +
taiga/base/fields.py | 1 +
taiga/base/filters.py | 1 +
taiga/base/formats/en/formats.py | 1 +
taiga/base/formats/es/formats.py | 1 +
taiga/base/mails.py | 1 +
taiga/base/management/commands/test_emails.py | 1 +
taiga/base/middleware/cors.py | 1 +
taiga/base/neighbors.py | 1 +
taiga/base/response.py | 1 +
taiga/base/routers.py | 1 +
taiga/base/signals/cleanup_files.py | 1 +
taiga/base/signals/thumbnails.py | 1 +
taiga/base/status.py | 1 +
taiga/base/storage.py | 1 +
taiga/base/tags.py | 1 +
taiga/base/throttling.py | 1 +
taiga/base/utils/contenttypes.py | 1 +
taiga/base/utils/db.py | 1 +
taiga/base/utils/dicts.py | 1 +
taiga/base/utils/diff.py | 1 +
taiga/base/utils/files.py | 1 +
taiga/base/utils/functions.py | 1 +
taiga/base/utils/iterators.py | 1 +
taiga/base/utils/json.py | 1 +
taiga/base/utils/sequence.py | 1 +
taiga/base/utils/signals.py | 1 +
taiga/base/utils/slug.py | 1 +
taiga/base/utils/text.py | 1 +
taiga/base/utils/thumbnails.py | 1 +
taiga/base/utils/urls.py | 1 +
taiga/celery.py | 1 +
taiga/deferred.py | 1 +
taiga/events/__init__.py | 1 +
taiga/events/apps.py | 1 +
taiga/events/backends/__init__.py | 1 +
taiga/events/backends/base.py | 1 +
taiga/events/backends/postgresql.py | 1 +
taiga/events/backends/rabbitmq.py | 1 +
taiga/events/events.py | 5 +++++
.../commands/emit_notification_message.py | 1 +
taiga/events/middleware.py | 5 +++++
taiga/events/signal_handlers.py | 1 +
taiga/export_import/api.py | 1 +
taiga/export_import/exceptions.py | 1 +
.../management/commands/dump_project.py | 1 +
.../management/commands/load_dump.py | 1 +
taiga/export_import/mixins.py | 1 +
taiga/export_import/permissions.py | 1 +
taiga/export_import/renderers.py | 1 +
taiga/export_import/serializers.py | 1 +
taiga/export_import/services/__init__.py | 1 +
taiga/export_import/services/render.py | 1 +
taiga/export_import/services/store.py | 1 +
taiga/export_import/tasks.py | 1 +
taiga/export_import/throttling.py | 1 +
taiga/external_apps/admin.py | 1 +
taiga/external_apps/api.py | 1 +
taiga/external_apps/auth_backends.py | 1 +
taiga/external_apps/encryption.py | 1 +
taiga/external_apps/models.py | 1 +
taiga/external_apps/permissions.py | 1 +
taiga/external_apps/serializers.py | 1 +
taiga/external_apps/services.py | 1 +
taiga/feedback/__init__.py | 1 +
taiga/feedback/admin.py | 1 +
taiga/feedback/api.py | 1 +
taiga/feedback/apps.py | 1 +
taiga/feedback/models.py | 1 +
taiga/feedback/permissions.py | 1 +
taiga/feedback/routers.py | 1 +
taiga/feedback/serializers.py | 1 +
taiga/feedback/services.py | 1 +
taiga/front/sitemaps/__init__.py | 5 +++--
taiga/front/sitemaps/base.py | 5 +++--
taiga/front/sitemaps/generics.py | 5 +++--
taiga/front/sitemaps/issues.py | 5 +++--
taiga/front/sitemaps/milestones.py | 5 +++--
taiga/front/sitemaps/projects.py | 5 +++--
taiga/front/sitemaps/tasks.py | 5 +++--
taiga/front/sitemaps/users.py | 5 +++--
taiga/front/sitemaps/userstories.py | 5 +++--
taiga/front/sitemaps/wiki.py | 5 +++--
taiga/front/templatetags/functions.py | 1 +
taiga/front/urls.py | 1 +
taiga/hooks/api.py | 1 +
taiga/hooks/bitbucket/api.py | 1 +
taiga/hooks/bitbucket/event_hooks.py | 1 +
taiga/hooks/bitbucket/models.py | 1 +
taiga/hooks/bitbucket/services.py | 1 +
taiga/hooks/event_hooks.py | 1 +
taiga/hooks/exceptions.py | 1 +
taiga/hooks/github/api.py | 1 +
taiga/hooks/github/event_hooks.py | 1 +
taiga/hooks/github/models.py | 1 +
taiga/hooks/github/services.py | 1 +
taiga/hooks/gitlab/api.py | 1 +
taiga/hooks/gitlab/event_hooks.py | 1 +
taiga/hooks/gitlab/models.py | 1 +
taiga/hooks/gitlab/services.py | 1 +
taiga/locale/api.py | 1 +
taiga/locale/permissions.py | 1 +
taiga/mdrender/extensions/autolink.py | 1 +
taiga/mdrender/extensions/automail.py | 1 +
taiga/mdrender/extensions/semi_sane_lists.py | 1 +
taiga/mdrender/extensions/spaced_link.py | 1 +
taiga/mdrender/extensions/strikethrough.py | 1 +
taiga/mdrender/extensions/target_link.py | 1 +
taiga/mdrender/extensions/wikilinks.py | 1 +
taiga/mdrender/service.py | 1 +
taiga/mdrender/templatetags/__init__.py | 1 -
taiga/mdrender/templatetags/functions.py | 1 +
taiga/permissions/choices.py | 1 +
taiga/permissions/permissions.py | 1 +
taiga/permissions/services.py | 1 +
taiga/projects/__init__.py | 1 +
taiga/projects/admin.py | 1 +
taiga/projects/api.py | 1 +
taiga/projects/apps.py | 1 +
taiga/projects/attachments/__init__.py | 1 +
taiga/projects/attachments/admin.py | 1 +
taiga/projects/attachments/api.py | 1 +
taiga/projects/attachments/apps.py | 1 +
.../management/commands/generate_sha1.py | 18 ++++++++++++++++++
taiga/projects/attachments/models.py | 1 +
taiga/projects/attachments/permissions.py | 1 +
taiga/projects/attachments/serializers.py | 1 +
taiga/projects/attachments/services.py | 1 +
taiga/projects/choices.py | 1 +
taiga/projects/custom_attributes/admin.py | 1 +
taiga/projects/custom_attributes/api.py | 1 +
taiga/projects/custom_attributes/choices.py | 1 +
taiga/projects/custom_attributes/models.py | 1 +
.../projects/custom_attributes/permissions.py | 1 +
.../projects/custom_attributes/serializers.py | 1 +
taiga/projects/custom_attributes/services.py | 1 +
taiga/projects/custom_attributes/signals.py | 1 +
taiga/projects/filters.py | 1 +
taiga/projects/history/api.py | 1 +
taiga/projects/history/choices.py | 4 ++++
taiga/projects/history/freeze_impl.py | 1 +
taiga/projects/history/mixins.py | 1 +
taiga/projects/history/models.py | 4 ++++
taiga/projects/history/permissions.py | 1 +
taiga/projects/history/serializers.py | 1 +
taiga/projects/history/services.py | 4 ++++
.../projects/history/templatetags/__init__.py | 1 -
.../history/templatetags/functions.py | 1 +
taiga/projects/issues/__init__.py | 1 +
taiga/projects/issues/admin.py | 1 +
taiga/projects/issues/api.py | 1 +
taiga/projects/issues/apps.py | 1 +
taiga/projects/issues/models.py | 1 +
taiga/projects/issues/permissions.py | 1 +
taiga/projects/issues/serializers.py | 1 +
taiga/projects/issues/services.py | 1 +
taiga/projects/issues/signals.py | 1 +
taiga/projects/likes/admin.py | 1 +
taiga/projects/likes/mixins/serializers.py | 1 +
taiga/projects/likes/mixins/viewsets.py | 1 +
taiga/projects/likes/models.py | 1 +
taiga/projects/likes/serializers.py | 1 +
taiga/projects/likes/services.py | 1 +
.../management/commands/sample_data.py | 1 +
taiga/projects/milestones/admin.py | 1 +
taiga/projects/milestones/api.py | 1 +
taiga/projects/milestones/models.py | 1 +
taiga/projects/milestones/permissions.py | 1 +
taiga/projects/milestones/serializers.py | 1 +
taiga/projects/milestones/services.py | 1 +
taiga/projects/milestones/validators.py | 18 ++++++++++++++++++
taiga/projects/mixins/blocked.py | 1 +
taiga/projects/mixins/on_destroy.py | 1 +
taiga/projects/mixins/ordering.py | 1 +
taiga/projects/mixins/serializers.py | 1 +
taiga/projects/models.py | 1 +
taiga/projects/notifications/admin.py | 1 +
taiga/projects/notifications/api.py | 1 +
taiga/projects/notifications/choices.py | 1 +
.../management/commands/send_notifications.py | 1 +
taiga/projects/notifications/mixins.py | 1 +
taiga/projects/notifications/models.py | 1 +
taiga/projects/notifications/permissions.py | 1 +
taiga/projects/notifications/serializers.py | 1 +
taiga/projects/notifications/services.py | 1 +
taiga/projects/notifications/utils.py | 1 +
taiga/projects/notifications/validators.py | 1 +
taiga/projects/occ/__init__.py | 1 +
taiga/projects/occ/mixins.py | 1 +
taiga/projects/permissions.py | 1 +
taiga/projects/references/api.py | 1 +
taiga/projects/references/models.py | 1 +
taiga/projects/references/permissions.py | 1 +
taiga/projects/references/sequences.py | 1 +
taiga/projects/references/serializers.py | 1 +
taiga/projects/references/services.py | 1 +
taiga/projects/serializers.py | 1 +
taiga/projects/services/__init__.py | 1 +
taiga/projects/services/bulk_update_order.py | 1 +
taiga/projects/services/filters.py | 1 +
taiga/projects/services/invitations.py | 18 ++++++++++++++++++
taiga/projects/services/logo.py | 1 +
taiga/projects/services/members.py | 1 +
taiga/projects/services/modules_config.py | 1 +
taiga/projects/services/projects.py | 1 +
taiga/projects/services/stats.py | 1 +
taiga/projects/services/tags_colors.py | 1 +
taiga/projects/services/transfer.py | 1 +
taiga/projects/signals.py | 1 +
taiga/projects/tasks/__init__.py | 1 +
taiga/projects/tasks/admin.py | 1 +
taiga/projects/tasks/api.py | 1 +
taiga/projects/tasks/apps.py | 1 +
taiga/projects/tasks/models.py | 1 +
taiga/projects/tasks/permissions.py | 1 +
taiga/projects/tasks/serializers.py | 1 +
taiga/projects/tasks/services.py | 1 +
taiga/projects/tasks/signals.py | 1 +
taiga/projects/tasks/validators.py | 18 ++++++++++++++++++
taiga/projects/translations.py | 1 +
taiga/projects/userstories/__init__.py | 1 +
taiga/projects/userstories/admin.py | 1 +
taiga/projects/userstories/api.py | 1 +
taiga/projects/userstories/apps.py | 1 +
taiga/projects/userstories/models.py | 1 +
taiga/projects/userstories/permissions.py | 1 +
taiga/projects/userstories/serializers.py | 1 +
taiga/projects/userstories/services.py | 1 +
taiga/projects/userstories/signals.py | 1 +
taiga/projects/userstories/validators.py | 1 +
taiga/projects/validators.py | 1 +
taiga/projects/votes/admin.py | 1 +
taiga/projects/votes/mixins/serializers.py | 1 +
taiga/projects/votes/mixins/viewsets.py | 1 +
taiga/projects/votes/models.py | 1 +
taiga/projects/votes/serializers.py | 1 +
taiga/projects/votes/services.py | 1 +
taiga/projects/votes/utils.py | 1 +
taiga/projects/wiki/admin.py | 1 +
taiga/projects/wiki/api.py | 1 +
taiga/projects/wiki/models.py | 1 +
taiga/projects/wiki/permissions.py | 1 +
taiga/projects/wiki/serializers.py | 1 +
taiga/routers.py | 1 +
taiga/searches/api.py | 1 +
taiga/searches/serializers.py | 1 +
taiga/searches/services.py | 1 +
taiga/stats/__init__.py | 1 +
taiga/stats/api.py | 1 +
taiga/stats/apps.py | 1 +
taiga/stats/permissions.py | 1 +
taiga/stats/routers.py | 1 +
taiga/stats/services.py | 1 +
taiga/timeline/__init__.py | 1 +
taiga/timeline/api.py | 1 +
taiga/timeline/apps.py | 1 +
...lear_unnecessary_new_membership_entries.py | 1 +
.../_rebuild_timeline_for_user_creation.py | 1 +
.../_update_timeline_for_updated_tasks.py | 1 +
.../management/commands/rebuild_timeline.py | 1 +
...rebuild_timeline_iterating_per_projects.py | 1 +
taiga/timeline/models.py | 1 +
taiga/timeline/permissions.py | 1 +
taiga/timeline/serializers.py | 1 +
taiga/timeline/service.py | 1 +
taiga/timeline/signals.py | 1 +
taiga/timeline/timeline_implementations.py | 1 +
taiga/urls.py | 1 +
taiga/users/admin.py | 1 +
taiga/users/api.py | 1 +
taiga/users/filters.py | 1 +
taiga/users/forms.py | 1 +
taiga/users/gravatar.py | 1 +
taiga/users/models.py | 1 +
taiga/users/permissions.py | 1 +
taiga/users/serializers.py | 1 +
taiga/users/services.py | 1 +
taiga/users/signals.py | 1 +
taiga/users/validators.py | 1 +
taiga/userstorage/api.py | 1 +
taiga/userstorage/filters.py | 1 +
taiga/userstorage/models.py | 1 +
taiga/userstorage/permissions.py | 1 +
taiga/userstorage/serializers.py | 1 +
taiga/webhooks/__init__.py | 1 +
taiga/webhooks/api.py | 1 +
taiga/webhooks/apps.py | 1 +
taiga/webhooks/models.py | 1 +
taiga/webhooks/permissions.py | 1 +
taiga/webhooks/serializers.py | 1 +
taiga/webhooks/signal_handlers.py | 1 +
taiga/webhooks/tasks.py | 1 +
taiga/wsgi.py | 19 +++++++++++++++++++
tests/conftest.py | 1 +
tests/factories.py | 1 +
tests/fixtures.py | 1 +
.../test_application_tokens_resources.py | 1 +
.../test_attachment_resources.py | 1 +
.../test_auth_resources.py | 1 +
.../resources_permissions/test_feedback.py | 1 +
.../test_history_resources.py | 1 +
.../test_issues_custom_attributes_resource.py | 1 +
.../test_issues_resources.py | 1 +
.../test_milestones_resources.py | 1 +
.../test_modules_resources.py | 1 +
.../test_projects_choices_resources.py | 1 +
.../test_projects_resource.py | 1 +
.../test_resolver_resources.py | 1 +
.../test_search_resources.py | 1 +
.../test_storage_resources.py | 1 +
.../test_tasks_custom_attributes_resource.py | 1 +
.../test_tasks_resources.py | 1 +
.../test_timelines_resources.py | 1 +
.../test_users_resources.py | 1 +
..._userstories_custom_attributes_resource.py | 1 +
.../test_userstories_resources.py | 1 +
.../test_webhooks_resources.py | 1 +
.../test_wiki_resources.py | 1 +
tests/integration/test_application_tokens.py | 1 +
tests/integration/test_attachments.py | 1 +
tests/integration/test_auth_api.py | 1 +
.../test_custom_attributes_issues.py | 1 +
.../test_custom_attributes_tasks.py | 1 +
.../test_custom_attributes_user_stories.py | 1 +
tests/integration/test_exporter_api.py | 1 +
tests/integration/test_fan_projects.py | 1 +
tests/integration/test_feedback.py | 1 +
tests/integration/test_history.py | 1 +
tests/integration/test_hooks_bitbucket.py | 1 +
tests/integration/test_hooks_github.py | 1 +
tests/integration/test_hooks_gitlab.py | 1 +
tests/integration/test_importer_api.py | 1 +
tests/integration/test_issues.py | 1 +
tests/integration/test_mdrender.py | 1 +
tests/integration/test_memberships.py | 1 +
tests/integration/test_milestones.py | 1 +
tests/integration/test_models.py | 1 +
tests/integration/test_neighbors.py | 1 +
tests/integration/test_notifications.py | 1 +
tests/integration/test_occ.py | 1 +
tests/integration/test_permissions.py | 1 +
tests/integration/test_projects.py | 1 +
.../integration/test_references_sequences.py | 1 +
tests/integration/test_roles.py | 1 +
tests/integration/test_searches.py | 1 +
tests/integration/test_stats.py | 1 +
tests/integration/test_tasks.py | 1 +
tests/integration/test_throwttling.py | 1 +
tests/integration/test_timeline.py | 1 +
tests/integration/test_totals_projects.py | 1 +
tests/integration/test_us_autoclosing.py | 1 +
tests/integration/test_users.py | 1 +
tests/integration/test_userstorage_api.py | 1 +
tests/integration/test_userstories.py | 1 +
tests/integration/test_vote_issues.py | 1 +
tests/integration/test_vote_tasks.py | 1 +
tests/integration/test_vote_userstories.py | 1 +
tests/integration/test_votes.py | 1 +
tests/integration/test_watch_issues.py | 1 +
tests/integration/test_watch_milestones.py | 1 +
tests/integration/test_watch_projects.py | 1 +
tests/integration/test_watch_tasks.py | 1 +
tests/integration/test_watch_userstories.py | 1 +
tests/integration/test_watch_wikipages.py | 1 +
tests/integration/test_webhooks_issues.py | 1 +
tests/integration/test_webhooks_milestones.py | 1 +
tests/integration/test_webhooks_signals.py | 1 +
tests/integration/test_webhooks_tasks.py | 1 +
.../integration/test_webhooks_userstories.py | 1 +
tests/integration/test_webhooks_wikipages.py | 1 +
tests/models.py | 1 +
tests/unit/conftest.py | 1 +
tests/unit/test_base_api_permissions.py | 1 +
tests/unit/test_deferred.py | 1 +
tests/unit/test_export.py | 1 +
tests/unit/test_gravatar.py | 1 +
tests/unit/test_mdrender.py | 1 +
tests/unit/test_serializer_mixins.py | 1 +
tests/unit/test_slug.py | 1 +
tests/unit/test_timeline.py | 1 +
tests/unit/test_tokens.py | 1 +
tests/unit/test_utils.py | 1 +
tests/utils.py | 1 +
432 files changed, 570 insertions(+), 23 deletions(-)
diff --git a/manage.py b/manage.py
index f9726f9e..63a6358b 100755
--- a/manage.py
+++ b/manage.py
@@ -1,4 +1,23 @@
#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
import os
import sys
diff --git a/settings/__init__.py b/settings/__init__.py
index 23fd84b8..a5fbfb3e 100644
--- a/settings/__init__.py
+++ b/settings/__init__.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/celery.py b/settings/celery.py
index 05444695..82e4d92f 100644
--- a/settings/celery.py
+++ b/settings/celery.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/common.py b/settings/common.py
index 568be60c..f227d97c 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/development.py b/settings/development.py
index 6a7f981d..8611719d 100644
--- a/settings/development.py
+++ b/settings/development.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/local.py.example b/settings/local.py.example
index 09e53ca4..4ae5a8ab 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/sr.py b/settings/sr.py
index bc58e8e0..1477e682 100644
--- a/settings/sr.py
+++ b/settings/sr.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/testing.py b/settings/testing.py
index 6862a5b1..29dd67d2 100644
--- a/settings/testing.py
+++ b/settings/testing.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/settings/travis.py b/settings/travis.py
index 8481688c..13c0f9c2 100644
--- a/settings/travis.py
+++ b/settings/travis.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/__init__.py b/taiga/__init__.py
index ee793c4a..8be9bc25 100644
--- a/taiga/__init__.py
+++ b/taiga/__init__.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/__init__.py b/taiga/auth/__init__.py
index 8b137891..e69de29b 100644
--- a/taiga/auth/__init__.py
+++ b/taiga/auth/__init__.py
@@ -1 +0,0 @@
-
diff --git a/taiga/auth/api.py b/taiga/auth/api.py
index 7f3d7ad8..5d14d18f 100644
--- a/taiga/auth/api.py
+++ b/taiga/auth/api.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py
index 920d3f20..7813d457 100644
--- a/taiga/auth/backends.py
+++ b/taiga/auth/backends.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/permissions.py b/taiga/auth/permissions.py
index 3e648abd..854eff26 100644
--- a/taiga/auth/permissions.py
+++ b/taiga/auth/permissions.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
# Copyright (C) 2014-2016 Alejandro Alonso
diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py
index 52a1ae17..8e8df4e2 100644
--- a/taiga/auth/serializers.py
+++ b/taiga/auth/serializers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/services.py b/taiga/auth/services.py
index 5015a02e..a76c350f 100644
--- a/taiga/auth/services.py
+++ b/taiga/auth/services.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/signals.py b/taiga/auth/signals.py
index ed5782e9..9e375836 100644
--- a/taiga/auth/signals.py
+++ b/taiga/auth/signals.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py
index 58fe2938..a939b748 100644
--- a/taiga/auth/tokens.py
+++ b/taiga/auth/tokens.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/__init__.py b/taiga/base/__init__.py
index 1e06114a..5a7db2f0 100644
--- a/taiga/base/__init__.py
+++ b/taiga/base/__init__.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py
index b2c17b0d..ad481185 100644
--- a/taiga/base/api/__init__.py
+++ b/taiga/base/api/__init__.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/authentication.py b/taiga/base/api/authentication.py
index 40385395..d44c69d5 100644
--- a/taiga/base/api/authentication.py
+++ b/taiga/base/api/authentication.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py
index 365e4070..7dfa2c0a 100644
--- a/taiga/base/api/fields.py
+++ b/taiga/base/api/fields.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py
index 82d3f487..158d712d 100644
--- a/taiga/base/api/generics.py
+++ b/taiga/base/api/generics.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py
index 1125fcd9..89af6984 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/negotiation.py b/taiga/base/api/negotiation.py
index fdc16717..fd2a7028 100644
--- a/taiga/base/api/negotiation.py
+++ b/taiga/base/api/negotiation.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/pagination.py b/taiga/base/api/pagination.py
index 147a7bb7..46c1540d 100644
--- a/taiga/base/api/pagination.py
+++ b/taiga/base/api/pagination.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/parsers.py b/taiga/base/api/parsers.py
index 920a78cb..42d436f5 100644
--- a/taiga/base/api/parsers.py
+++ b/taiga/base/api/parsers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py
index 19b366fc..b03d6c18 100644
--- a/taiga/base/api/permissions.py
+++ b/taiga/base/api/permissions.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py
index f60c8ac5..60ba9a6e 100644
--- a/taiga/base/api/relations.py
+++ b/taiga/base/api/relations.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/renderers.py b/taiga/base/api/renderers.py
index 21c053e0..bc26e95b 100644
--- a/taiga/base/api/renderers.py
+++ b/taiga/base/api/renderers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/request.py b/taiga/base/api/request.py
index 3083b06d..059ece06 100644
--- a/taiga/base/api/request.py
+++ b/taiga/base/api/request.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/reverse.py b/taiga/base/api/reverse.py
index 2451fc1c..4d4de867 100644
--- a/taiga/base/api/reverse.py
+++ b/taiga/base/api/reverse.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index 55ae824f..a9e5f139 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py
index 9b894be6..1a3d01ba 100644
--- a/taiga/base/api/settings.py
+++ b/taiga/base/api/settings.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/templatetags/api.py b/taiga/base/api/templatetags/api.py
index b2710a18..c40b0aa4 100644
--- a/taiga/base/api/templatetags/api.py
+++ b/taiga/base/api/templatetags/api.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/throttling.py b/taiga/base/api/throttling.py
index 3ff19ff7..b23bea09 100644
--- a/taiga/base/api/throttling.py
+++ b/taiga/base/api/throttling.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/urlpatterns.py b/taiga/base/api/urlpatterns.py
index 8bfd6f1c..33548a07 100644
--- a/taiga/base/api/urlpatterns.py
+++ b/taiga/base/api/urlpatterns.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/urls.py b/taiga/base/api/urls.py
index 30b2cf4a..01be0d71 100644
--- a/taiga/base/api/urls.py
+++ b/taiga/base/api/urls.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils.py b/taiga/base/api/utils.py
index 77148b49..30318d5e 100644
--- a/taiga/base/api/utils.py
+++ b/taiga/base/api/utils.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils/__init__.py b/taiga/base/api/utils/__init__.py
index 227f56fd..b6198a45 100644
--- a/taiga/base/api/utils/__init__.py
+++ b/taiga/base/api/utils/__init__.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils/breadcrumbs.py b/taiga/base/api/utils/breadcrumbs.py
index 57965bce..0d10758f 100644
--- a/taiga/base/api/utils/breadcrumbs.py
+++ b/taiga/base/api/utils/breadcrumbs.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py
index 0f878914..be307d25 100644
--- a/taiga/base/api/utils/encoders.py
+++ b/taiga/base/api/utils/encoders.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils/formatting.py b/taiga/base/api/utils/formatting.py
index fbeb2534..f2decc0b 100644
--- a/taiga/base/api/utils/formatting.py
+++ b/taiga/base/api/utils/formatting.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/utils/mediatypes.py b/taiga/base/api/utils/mediatypes.py
index 02e09b5d..c0dc1266 100644
--- a/taiga/base/api/utils/mediatypes.py
+++ b/taiga/base/api/utils/mediatypes.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py
index 791523d4..8c6dfd09 100644
--- a/taiga/base/api/views.py
+++ b/taiga/base/api/views.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py
index af2d2789..95b09055 100644
--- a/taiga/base/api/viewsets.py
+++ b/taiga/base/api/viewsets.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/apps.py b/taiga/base/apps.py
index b56aaafb..f5f1879b 100644
--- a/taiga/base/apps.py
+++ b/taiga/base/apps.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/connectors/exceptions.py b/taiga/base/connectors/exceptions.py
index 0aca19fd..eb47c5db 100644
--- a/taiga/base/connectors/exceptions.py
+++ b/taiga/base/connectors/exceptions.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py
index afddb058..5700e75b 100644
--- a/taiga/base/decorators.py
+++ b/taiga/base/decorators.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index 104ba896..cc58ee6d 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index e45dace8..3f6fcf19 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index ea962af0..1cd19e64 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/formats/en/formats.py b/taiga/base/formats/en/formats.py
index 5046462e..37d64ee8 100644
--- a/taiga/base/formats/en/formats.py
+++ b/taiga/base/formats/en/formats.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/formats/es/formats.py b/taiga/base/formats/es/formats.py
index ab561c7f..f6d7dc55 100644
--- a/taiga/base/formats/es/formats.py
+++ b/taiga/base/formats/es/formats.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/mails.py b/taiga/base/mails.py
index 51be494d..48e6f296 100644
--- a/taiga/base/mails.py
+++ b/taiga/base/mails.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py
index 53a63a87..631bcd42 100644
--- a/taiga/base/management/commands/test_emails.py
+++ b/taiga/base/management/commands/test_emails.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py
index 101ff7a2..c7e2c615 100644
--- a/taiga/base/middleware/cors.py
+++ b/taiga/base/middleware/cors.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py
index 7a2f079e..a57d2eeb 100644
--- a/taiga/base/neighbors.py
+++ b/taiga/base/neighbors.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/response.py b/taiga/base/response.py
index 5b84123a..82d7794f 100644
--- a/taiga/base/response.py
+++ b/taiga/base/response.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/routers.py b/taiga/base/routers.py
index 4cf8a6f6..56b80f8e 100644
--- a/taiga/base/routers.py
+++ b/taiga/base/routers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/signals/cleanup_files.py b/taiga/base/signals/cleanup_files.py
index 0efab210..e2449ce2 100644
--- a/taiga/base/signals/cleanup_files.py
+++ b/taiga/base/signals/cleanup_files.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/signals/thumbnails.py b/taiga/base/signals/thumbnails.py
index 53bc1bca..e40cba07 100644
--- a/taiga/base/signals/thumbnails.py
+++ b/taiga/base/signals/thumbnails.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/status.py b/taiga/base/status.py
index 003c771b..c271e030 100644
--- a/taiga/base/status.py
+++ b/taiga/base/status.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/storage.py b/taiga/base/storage.py
index 84c2a460..a0b962c4 100644
--- a/taiga/base/storage.py
+++ b/taiga/base/storage.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/tags.py b/taiga/base/tags.py
index 8ba99e59..0e1cd866 100644
--- a/taiga/base/tags.py
+++ b/taiga/base/tags.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/throttling.py b/taiga/base/throttling.py
index b9cae88d..c0577ce0 100644
--- a/taiga/base/throttling.py
+++ b/taiga/base/throttling.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/utils/contenttypes.py b/taiga/base/utils/contenttypes.py
index 6b7a8e37..252a3db2 100644
--- a/taiga/base/utils/contenttypes.py
+++ b/taiga/base/utils/contenttypes.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py
index 2e076021..6569069d 100644
--- a/taiga/base/utils/db.py
+++ b/taiga/base/utils/db.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/utils/dicts.py b/taiga/base/utils/dicts.py
index 232832ff..23b90f17 100644
--- a/taiga/base/utils/dicts.py
+++ b/taiga/base/utils/dicts.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
diff --git a/taiga/base/utils/diff.py b/taiga/base/utils/diff.py
index 4b18a071..e08584ae 100644
--- a/taiga/base/utils/diff.py
+++ b/taiga/base/utils/diff.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán