From f1e4e82109d921150817b65f13f8938794498f83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Sat, 30 Jan 2016 15:44:05 +0100
Subject: [PATCH 001/105] Fix #3831
---
taiga/permissions/service.py | 4 ++--
taiga/projects/admin.py | 3 +++
taiga/projects/models.py | 8 +++++++-
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index ec64a95e..10c1f3b8 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -117,5 +117,5 @@ def set_base_permissions_for_project(project):
If a project is public anonymous and registered users should have at least visualization permissions
"""
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
- project.anon_permissions = list(set(project.anon_permissions + anon_permissions))
- project.public_permissions = list(set(project.public_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))
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 52022e20..18bca9c5 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -76,6 +76,9 @@ class ProjectAdmin(admin.ModelAdmin):
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 get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
return self.obj
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 89f8b5f9..b5576a3a 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -303,6 +303,12 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
if not self.is_looking_for_people:
self.looking_for_people_note = ""
+ if self.anon_permissions == None:
+ self.anon_permissions = []
+
+ if self.public_permissions == None:
+ self.public_permissions = []
+
super().save(*args, **kwargs)
def refresh_totals(self, save=True):
@@ -440,7 +446,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
from taiga.projects.userstories.apps import connect_all_userstories_signals, disconnect_all_userstories_signals
from taiga.projects.issues.apps import connect_all_issues_signals, disconnect_all_issues_signals
from taiga.projects.apps import connect_memberships_signals, disconnect_memberships_signals
-
+
disconnect_events_signals()
disconnect_all_issues_signals()
disconnect_all_tasks_signals()
From 4ce824223f4e5065d4a2384375d6f31d1689e0cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 1 Feb 2016 11:22:07 +0100
Subject: [PATCH 002/105] Add discover url to the sitemap
---
taiga/front/sitemaps/generics.py | 1 +
taiga/front/urls.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/taiga/front/sitemaps/generics.py b/taiga/front/sitemaps/generics.py
index f4113189..241316e1 100644
--- a/taiga/front/sitemaps/generics.py
+++ b/taiga/front/sitemaps/generics.py
@@ -27,6 +27,7 @@ class GenericSitemap(Sitemap):
def items(self):
return [
{"url_key": "home", "changefreq": "monthly", "priority": 1},
+ {"url_key": "discover", "changefreq": "daily", "priority": 1},
{"url_key": "login", "changefreq": "monthly", "priority": 1},
{"url_key": "register", "changefreq": "monthly", "priority": 1},
{"url_key": "forgot-password", "changefreq": "monthly", "priority": 1}
diff --git a/taiga/front/urls.py b/taiga/front/urls.py
index f39efa30..76709327 100644
--- a/taiga/front/urls.py
+++ b/taiga/front/urls.py
@@ -18,6 +18,7 @@
urls = {
"home": "/",
+ "discover": "/discover",
"login": "/login",
"register": "/register",
"forgot-password": "/forgot-password",
From d4f34b655418cbb422ffc32e00a26698c70a2c5a Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 1 Feb 2016 09:12:13 +0100
Subject: [PATCH 003/105] Issue 3842: Catch properly invalid IP's errors for
integrations
---
taiga/hooks/bitbucket/api.py | 12 +++++++++++-
taiga/hooks/gitlab/api.py | 11 ++++++++++-
tests/integration/test_hooks_bitbucket.py | 20 ++++++++++++++++++++
tests/integration/test_hooks_gitlab.py | 20 ++++++++++++++++++++
4 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py
index afd3c47b..6fcfcab5 100644
--- a/taiga/hooks/bitbucket/api.py
+++ b/taiga/hooks/bitbucket/api.py
@@ -25,6 +25,7 @@ from taiga.hooks.api import BaseWebhookApiViewSet
from . import event_hooks
from netaddr import all_matching_cidrs
+from netaddr.core import AddrFormatError
from urllib.parse import parse_qs
from ipware.ip import get_ip
@@ -56,7 +57,16 @@ class BitBucketViewSet(BaseWebhookApiViewSet):
valid_origin_ips = bitbucket_config.get("valid_origin_ips",
settings.BITBUCKET_VALID_ORIGIN_IPS)
origin_ip = get_ip(request)
- if valid_origin_ips and (len(all_matching_cidrs(origin_ip,valid_origin_ips)) == 0):
+ mathching_origin_ip = True
+
+ if valid_origin_ips:
+ try:
+ mathching_origin_ip = len(all_matching_cidrs(origin_ip,valid_origin_ips)) > 0
+
+ except AddrFormatError:
+ mathching_origin_ip = False
+
+ if not mathching_origin_ip:
return False
return project_secret == secret_key
diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py
index 89f6a5c8..6dd2368d 100644
--- a/taiga/hooks/gitlab/api.py
+++ b/taiga/hooks/gitlab/api.py
@@ -27,6 +27,7 @@ from taiga.hooks.api import BaseWebhookApiViewSet
from . import event_hooks
from netaddr import all_matching_cidrs
+from netaddr.core import AddrFormatError
class GitLabViewSet(BaseWebhookApiViewSet):
event_hook_classes = {
@@ -54,8 +55,16 @@ class GitLabViewSet(BaseWebhookApiViewSet):
gitlab_config = project.modules_config.config.get("gitlab", {})
valid_origin_ips = gitlab_config.get("valid_origin_ips", settings.GITLAB_VALID_ORIGIN_IPS)
origin_ip = get_ip(request)
+ mathching_origin_ip = True
- if valid_origin_ips and (len(all_matching_cidrs(origin_ip,valid_origin_ips)) == 0):
+ if valid_origin_ips:
+ try:
+ mathching_origin_ip = len(all_matching_cidrs(origin_ip,valid_origin_ips)) > 0
+
+ except AddrFormatError:
+ mathching_origin_ip = False
+
+ if not mathching_origin_ip:
return False
return project_secret == secret_key
diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py
index c21ca332..1dbf8ea2 100644
--- a/tests/integration/test_hooks_bitbucket.py
+++ b/tests/integration/test_hooks_bitbucket.py
@@ -99,6 +99,26 @@ def test_invalid_ip(client):
assert response.status_code == 400
+def test_invalid_origin_ip_settings(client):
+ project = f.ProjectFactory()
+ f.ProjectModulesConfigFactory(project=project, config={
+ "bitbucket": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["testing"]
+ }
+ })
+
+ url = reverse("bitbucket-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = json.dumps({"push": {"changes": [{"new": {"target": { "message": "test message"}}}]}})
+ response = client.post(url,
+ data,
+ content_type="application/json",
+ HTTP_X_EVENT_KEY="repo:push",
+ REMOTE_ADDR="111.111.111.112")
+ assert response.status_code == 400
+
+
def test_valid_local_network_ip(client):
project = f.ProjectFactory()
f.ProjectModulesConfigFactory(project=project, config={
diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py
index cd0c2b8b..cc2f8e66 100644
--- a/tests/integration/test_hooks_gitlab.py
+++ b/tests/integration/test_hooks_gitlab.py
@@ -99,6 +99,26 @@ def test_invalid_ip(client):
assert response.status_code == 400
+def test_invalid_origin_ip_settings(client):
+ project = f.ProjectFactory()
+ f.ProjectModulesConfigFactory(project=project, config={
+ "gitlab": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["testing"]
+ }
+ })
+
+ url = reverse("gitlab-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = {"test:": "data"}
+ response = client.post(url,
+ json.dumps(data),
+ content_type="application/json",
+ REMOTE_ADDR="111.111.111.112")
+
+ assert response.status_code == 400
+
+
def test_valid_local_network_ip(client):
project = f.ProjectFactory()
f.ProjectModulesConfigFactory(project=project, config={
From e15e00336dbbc5e3ab88aa351de43b062786ca7c Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 28 Jan 2016 12:33:55 +0100
Subject: [PATCH 004/105] When leaving a project or removing a membership the
project must allways have a valid owner
---
taiga/projects/api.py | 4 ++--
taiga/projects/permissions.py | 3 +--
taiga/projects/serializers.py | 4 ++--
taiga/projects/services/__init__.py | 2 +-
taiga/projects/services/members.py | 14 +++++++----
tests/integration/test_projects.py | 36 ++++++++++++++++++++++++++---
6 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index cede5ec9..2ed19f97 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -489,7 +489,7 @@ class MembershipViewSet(ModelCrudViewSet):
def get_serializer_class(self):
use_admin_serializer = False
-
+
if self.action == "create":
use_admin_serializer = True
@@ -544,7 +544,7 @@ class MembershipViewSet(ModelCrudViewSet):
def pre_delete(self, obj):
if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project):
- raise exc.BadRequest(_("At least one of the user must be an active admin"))
+ raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin"))
def pre_save(self, obj):
if not obj.token:
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index c1e017a7..ee96523f 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -37,8 +37,7 @@ class CanLeaveProject(PermissionComponent):
try:
if not services.can_user_leave_project(request.user, obj):
- raise exc.PermissionDenied(_("You can't leave the project if there are no "
- "more owners"))
+ 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:
return False
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 6147c962..a313b8a6 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -257,8 +257,8 @@ class MembershipSerializer(serializers.ModelSerializer):
project = self.object.project
if (self.object and
- not services.project_has_valid_owners(project, exclude_user=self.object.user)):
- raise serializers.ValidationError(_("At least one of the user must be an active admin"))
+ not services.project_has_valid_admins(project, exclude_user=self.object.user)):
+ raise serializers.ValidationError(_("The project must have an owner and at least one of the users must be an active admin"))
return attrs
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index d4940c37..b6a51e3e 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -37,7 +37,7 @@ from .logo import get_logo_big_thumbnail_url
from .members import create_members_in_bulk
from .members import get_members_from_bulk
-from .members import remove_user_from_project, project_has_valid_owners, can_user_leave_project
+from .members import remove_user_from_project, project_has_valid_admins, can_user_leave_project
from .modules_config import get_modules_config
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index f4efc5e9..c883156c 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -36,15 +36,15 @@ def remove_user_from_project(user, project):
models.Membership.objects.get(project=project, user=user).delete()
-def project_has_valid_owners(project, exclude_user=None):
+def project_has_valid_admins(project, exclude_user=None):
"""
Checks if the project has any owner membership with a user different than the specified
"""
- owner_memberships = project.memberships.filter(is_owner=True, user__is_active=True)
+ admin_memberships = project.memberships.filter(is_owner=True, user__is_active=True)
if exclude_user:
- owner_memberships = owner_memberships.exclude(user=exclude_user)
+ admin_memberships = admin_memberships.exclude(user=exclude_user)
- return owner_memberships.count() > 0
+ return admin_memberships.count() > 0
def can_user_leave_project(user, project):
@@ -52,7 +52,11 @@ def can_user_leave_project(user, project):
if not membership.is_owner:
return True
- if not project_has_valid_owners(project, exclude_user=user):
+ #The user can't leave if is the real owner of the project
+ if project.owner == user:
+ return False
+
+ if not project_has_valid_admins(project, exclude_user=user):
return False
return True
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 5349218c..8b079fea 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -249,7 +249,22 @@ def test_leave_project_valid_membership_only_owner(client):
url = reverse("projects-leave", args=(project.id,))
response = client.post(url)
assert response.status_code == 403
- assert response.data["_error_message"] == "You can't leave the project if there are no more owners"
+ assert response.data["_error_message"] == "You can't leave the project if you are the owner or there are no more admins"
+
+
+def test_leave_project_valid_membership_real_owner(client):
+ owner_user = f.UserFactory.create()
+ member_user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=owner_user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ f.MembershipFactory.create(project=project, user=owner_user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=member_user, role=role, is_owner=True)
+
+ client.login(owner_user)
+ url = reverse("projects-leave", args=(project.id,))
+ response = client.post(url)
+ assert response.status_code == 403
+ assert response.data["_error_message"] == "You can't leave the project if you are the owner or there are no more admins"
def test_leave_project_invalid_membership(client):
@@ -286,7 +301,22 @@ def test_delete_membership_only_owner(client):
url = reverse("memberships-detail", args=(membership.id,))
response = client.delete(url)
assert response.status_code == 400
- assert response.data["_error_message"] == "At least one of the user must be an active admin"
+ assert response.data["_error_message"] == "The project must have an owner and at least one of the users must be an active admin"
+
+
+def test_delete_membership_real_owner(client):
+ owner_user = f.UserFactory.create()
+ member_user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=owner_user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ owner_membership = f.MembershipFactory.create(project=project, user=owner_user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=member_user, role=role, is_owner=True)
+
+ client.login(owner_user)
+ url = reverse("memberships-detail", args=(owner_membership.id,))
+ response = client.delete(url)
+ assert response.status_code == 400
+ assert response.data["_error_message"] == "The project must have an owner and at least one of the users must be an active admin"
def test_edit_membership_only_owner(client):
@@ -301,7 +331,7 @@ def test_edit_membership_only_owner(client):
url = reverse("memberships-detail", args=(membership.id,))
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
- assert response.data["is_owner"][0] == "At least one of the user must be an active admin"
+ assert response.data["is_owner"][0] == "The project must have an owner and at least one of the users must be an active admin"
def test_anon_permissions_generation_when_making_project_public(client):
From 159f02c3bda59635a9d7db52121a521bec9fd217 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 2 Feb 2016 16:36:35 +0100
Subject: [PATCH 005/105] Update service.py
---
taiga/permissions/service.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index 10c1f3b8..2242c3ee 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -21,7 +21,6 @@ from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIO
from django.apps import apps
def _get_user_project_membership(user, project):
- Membership = apps.get_model("projects", "Membership")
if user.is_anonymous():
return None
From 5b39052ca26301c42ee7fac10707646842c117eb Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 25 Jan 2016 13:51:22 +0100
Subject: [PATCH 006/105] Blocking projects: updating project model and related
viewsets
---
settings/common.py | 1 +
taiga/base/api/mixins.py | 31 ++
taiga/base/api/viewsets.py | 2 +
taiga/base/exceptions.py | 5 +
taiga/base/status.py | 1 +
taiga/projects/api.py | 193 ++++++------
taiga/projects/attachments/api.py | 5 +-
taiga/projects/choices.py | 7 +
taiga/projects/custom_attributes/api.py | 9 +-
taiga/projects/issues/api.py | 10 +-
taiga/projects/likes/mixins/viewsets.py | 6 +
.../migrations/0035_project_blocked_code.py | 19 ++
taiga/projects/milestones/api.py | 4 +-
taiga/projects/mixins/ordering.py | 4 +-
taiga/projects/models.py | 5 +
taiga/projects/notifications/mixins.py | 8 +-
taiga/projects/serializers.py | 4 +-
taiga/projects/tasks/api.py | 11 +-
taiga/projects/userstories/api.py | 9 +-
taiga/projects/votes/mixins/viewsets.py | 15 +-
taiga/projects/wiki/api.py | 7 +-
taiga/users/api.py | 3 +-
taiga/webhooks/api.py | 10 +-
.../test_attachment_resources.py | 136 ++++++++-
.../test_issues_custom_attributes_resource.py | 77 ++++-
.../test_issues_resources.py | 105 ++++++-
.../test_milestones_resources.py | 69 ++++-
.../test_modules_resources.py | 209 +++++++++++++
.../test_projects_choices_resources.py | 277 ++++++++++++++++--
.../test_projects_resource.py | 95 +++++-
.../test_tasks_custom_attributes_resource.py | 76 ++++-
.../test_tasks_resources.py | 100 ++++++-
..._userstories_custom_attributes_resource.py | 81 ++++-
.../test_userstories_resources.py | 91 +++++-
.../test_webhooks_resources.py | 71 ++++-
.../test_wiki_resources.py | 93 +++++-
tests/integration/test_vote_tasks.py | 6 +-
tests/integration/test_vote_userstories.py | 6 +-
tests/integration/test_watch_issues.py | 4 +-
tests/integration/test_watch_tasks.py | 7 +-
tests/integration/test_watch_userstories.py | 10 +-
41 files changed, 1701 insertions(+), 181 deletions(-)
create mode 100644 taiga/projects/migrations/0035_project_blocked_code.py
create mode 100644 tests/integration/resources_permissions/test_modules_resources.py
diff --git a/settings/common.py b/settings/common.py
index 3687486b..e013b3f7 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -522,6 +522,7 @@ WEBHOOKS_ENABLED = False
FRONT_SITEMAP_ENABLED = False
FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
+EXTRA_BLOCKING_CODES = []
from .sr import *
diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py
index 27c05675..e07635ed 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -53,6 +53,8 @@ from taiga.base import response
from .settings import api_settings
from .utils import get_object_or_404
+from .. import exceptions as exc
+
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
@@ -237,3 +239,32 @@ class DestroyModelMixin:
obj.delete()
self.post_delete(obj)
return response.NoContent()
+
+
+class BlockeableModelMixin:
+ def is_blocked(self, obj):
+ raise NotImplementedError("is_blocked must be overridden")
+
+ def pre_conditions_blocked(self, obj):
+ #Raises permission exception
+ if obj is not None and self.is_blocked(obj):
+ raise exc.Blocked(_("Blocked element"))
+
+
+class BlockeableSaveMixin(BlockeableModelMixin):
+ def pre_conditions_on_save(self, obj):
+ # Called on create and update calls
+ self.pre_conditions_blocked(obj)
+ super().pre_conditions_on_save(obj)
+
+
+class BlockeableDeleteMixin():
+ def pre_conditions_on_delete(self, obj):
+ # Called on destroy call
+ self.pre_conditions_blocked(obj)
+ super().pre_conditions_on_delete(obj)
+
+
+class BlockedByProjectMixin(BlockeableSaveMixin, BlockeableDeleteMixin):
+ def is_blocked(self, obj):
+ return obj.project is not None and obj.project.blocked_code is not None
diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py
index 9c5f1e5d..af2d2789 100644
--- a/taiga/base/api/viewsets.py
+++ b/taiga/base/api/viewsets.py
@@ -187,11 +187,13 @@ class ModelListViewSet(mixins.RetrieveModelMixin,
GenericViewSet):
pass
+
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
GenericViewSet):
pass
+
class ModelRetrieveViewSet(mixins.RetrieveModelMixin,
GenericViewSet):
pass
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index 3e7c2104..f81c1424 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -201,6 +201,11 @@ class NotAuthenticated(NotAuthenticated):
pass
+class Blocked(APIException):
+ status_code = status.HTTP_451_BLOCKED
+ default_detail = _("Blocked element")
+
+
def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail
diff --git a/taiga/base/status.py b/taiga/base/status.py
index 08386721..003c771b 100644
--- a/taiga/base/status.py
+++ b/taiga/base/status.py
@@ -104,6 +104,7 @@ HTTP_417_EXPECTATION_FAILED = 417
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
+HTTP_451_BLOCKED = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 2ed19f97..2346ae29 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -33,6 +33,7 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
from taiga.base.api.permissions import AllowAnyPermission
from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.slug import slugify_uniquely
@@ -61,7 +62,9 @@ from . import services
######################################################
## Project
######################################################
-class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
+class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
+ BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
+
queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
@@ -87,6 +90,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
"total_activity_last_month",
"total_activity_last_year")
+ def is_blocked(self, obj):
+ return obj.blocked_code is not None
+
def _get_order_by_field_name(self):
order_by_query_param = project_filters.CanViewProjectObjFilterBackend.order_by_query_param
order_by = self.request.QUERY_PARAMS.get(order_by_query_param, None)
@@ -157,6 +163,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
except Exception:
raise exc.WrongArguments(_("Invalid image format"))
+ self.pre_conditions_on_save(self.object)
+
self.object.logo = logo
self.object.save(update_fields=["logo"])
@@ -170,7 +178,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
"""
self.object = get_object_or_404(self.get_queryset(), **kwargs)
self.check_permissions(request, "remove_logo", self.object)
-
+ self.pre_conditions_on_save(self.object)
self.object.logo = None
self.object.save(update_fields=["logo"])
@@ -181,6 +189,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
def watch(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "watch", project)
+ self.pre_conditions_on_save(project)
notify_level = request.DATA.get("notify_level", NotifyLevel.involved)
project.add_watcher(self.request.user, notify_level=notify_level)
return response.Ok()
@@ -189,6 +198,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
def unwatch(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "unwatch", project)
+ self.pre_conditions_on_save(project)
user = self.request.user
project.remove_watcher(user)
return response.Ok()
@@ -206,77 +216,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None)
- @list_route(methods=["GET"])
- def by_slug(self, request):
- slug = request.QUERY_PARAMS.get("slug", None)
- project = get_object_or_404(models.Project, slug=slug)
- return self.retrieve(request, pk=project.pk)
-
- @detail_route(methods=["GET", "PATCH"])
- def modules(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, 'modules', project)
- modules_config = services.get_modules_config(project)
-
- if request.method == "GET":
- return response.Ok(modules_config.config)
-
- else:
- modules_config.config.update(request.DATA)
- modules_config.save()
- return response.NoContent()
-
- @detail_route(methods=["GET"])
- def stats(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "stats", project)
- return response.Ok(services.get_stats_for_project(project))
-
- def _regenerate_csv_uuid(self, project, field):
- uuid_value = uuid.uuid4().hex
- setattr(project, field, uuid_value)
- project.save()
- return uuid_value
-
- @detail_route(methods=["POST"])
- def regenerate_userstories_csv_uuid(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "regenerate_userstories_csv_uuid", project)
- data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
- return response.Ok(data)
-
- @detail_route(methods=["POST"])
- def regenerate_issues_csv_uuid(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "regenerate_issues_csv_uuid", project)
- data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
- return response.Ok(data)
-
- @detail_route(methods=["POST"])
- def regenerate_tasks_csv_uuid(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "regenerate_tasks_csv_uuid", project)
- data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
- return response.Ok(data)
-
- @detail_route(methods=["GET"])
- def member_stats(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "member_stats", project)
- return response.Ok(services.get_member_stats_for_project(project))
-
- @detail_route(methods=["GET"])
- def issues_stats(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "issues_stats", project)
- return response.Ok(services.get_stats_for_project_issues(project))
-
- @detail_route(methods=["GET"])
- def tags_colors(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "tags_colors", project)
- return response.Ok(dict(project.tags_colors))
-
@detail_route(methods=["POST"])
def create_template(self, request, **kwargs):
template_name = request.DATA.get('template_name', None)
@@ -304,13 +243,89 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
template.save()
return response.Created(serializers.ProjectTemplateSerializer(template).data)
- @detail_route(methods=['post'])
+ @detail_route(methods=['POST'])
def leave(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, 'leave', project)
+ self.pre_conditions_on_save(project)
services.remove_user_from_project(request.user, project)
return response.Ok()
+ @detail_route(methods=["POST"])
+ def regenerate_userstories_csv_uuid(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "regenerate_userstories_csv_uuid", project)
+ self.pre_conditions_on_save(project)
+ data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
+ return response.Ok(data)
+
+ @detail_route(methods=["POST"])
+ def regenerate_issues_csv_uuid(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "regenerate_issues_csv_uuid", project)
+ self.pre_conditions_on_save(project)
+ data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
+ return response.Ok(data)
+
+ @detail_route(methods=["POST"])
+ def regenerate_tasks_csv_uuid(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "regenerate_tasks_csv_uuid", project)
+ self.pre_conditions_on_save(project)
+ data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
+ return response.Ok(data)
+
+ @list_route(methods=["GET"])
+ def by_slug(self, request):
+ slug = request.QUERY_PARAMS.get("slug", None)
+ project = get_object_or_404(models.Project, slug=slug)
+ return self.retrieve(request, pk=project.pk)
+
+ @detail_route(methods=["GET", "PATCH"])
+ def modules(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, 'modules', project)
+ modules_config = services.get_modules_config(project)
+
+ if request.method == "GET":
+ return response.Ok(modules_config.config)
+
+ else:
+ self.pre_conditions_on_save(project)
+ modules_config.config.update(request.DATA)
+ modules_config.save()
+ return response.NoContent()
+
+ @detail_route(methods=["GET"])
+ def stats(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "stats", project)
+ return response.Ok(services.get_stats_for_project(project))
+
+ def _regenerate_csv_uuid(self, project, field):
+ uuid_value = uuid.uuid4().hex
+ setattr(project, field, uuid_value)
+ project.save()
+ return uuid_value
+
+ @detail_route(methods=["GET"])
+ def member_stats(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "member_stats", project)
+ return response.Ok(services.get_member_stats_for_project(project))
+
+ @detail_route(methods=["GET"])
+ def issues_stats(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "issues_stats", project)
+ return response.Ok(services.get_stats_for_project_issues(project))
+
+ @detail_route(methods=["GET"])
+ def tags_colors(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "tags_colors", project)
+ return response.Ok(dict(project.tags_colors))
+
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:
@@ -364,7 +379,9 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
## Custom values for selectors
######################################################
-class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
+
model = models.Points
serializer_class = serializers.PointsSerializer
permission_classes = (permissions.PointsPermission,)
@@ -378,7 +395,9 @@ class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
move_on_destroy_project_default_field = "default_points"
-class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
+
model = models.UserStoryStatus
serializer_class = serializers.UserStoryStatusSerializer
permission_classes = (permissions.UserStoryStatusPermission,)
@@ -392,7 +411,9 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrd
move_on_destroy_project_default_field = "default_us_status"
-class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
+
model = models.TaskStatus
serializer_class = serializers.TaskStatusSerializer
permission_classes = (permissions.TaskStatusPermission,)
@@ -406,7 +427,9 @@ class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMix
move_on_destroy_project_default_field = "default_task_status"
-class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
+
model = models.Severity
serializer_class = serializers.SeveritySerializer
permission_classes = (permissions.SeverityPermission,)
@@ -420,7 +443,8 @@ class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
move_on_destroy_project_default_field = "default_severity"
-class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Priority
serializer_class = serializers.PrioritySerializer
permission_classes = (permissions.PriorityPermission,)
@@ -434,7 +458,8 @@ class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
move_on_destroy_project_default_field = "default_priority"
-class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueType
serializer_class = serializers.IssueTypeSerializer
permission_classes = (permissions.IssueTypePermission,)
@@ -448,7 +473,8 @@ class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixi
move_on_destroy_project_default_field = "default_issue_type"
-class IssueStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
+class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueStatus
serializer_class = serializers.IssueStatusSerializer
permission_classes = (permissions.IssueStatusPermission,)
@@ -479,7 +505,7 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
## Members & Invitations
######################################################
-class MembershipViewSet(ModelCrudViewSet):
+class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Membership
admin_serializer_class = serializers.MembershipAdminSerializer
serializer_class = serializers.MembershipSerializer
@@ -517,6 +543,8 @@ class MembershipViewSet(ModelCrudViewSet):
project = models.Project.objects.get(id=data["project_id"])
invitation_extra_text = data.get("invitation_extra_text", None)
self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
# TODO: this should be moved to main exception handler instead
# of handling explicit exception catchin here.
@@ -538,6 +566,7 @@ class MembershipViewSet(ModelCrudViewSet):
invitation = self.get_object()
self.check_permissions(request, 'resend_invitation', invitation.project)
+ self.pre_conditions_on_save(invitation)
services.send_invitation(invitation=invitation)
return response.NoContent()
diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py
index 65ad55b3..481021ed 100644
--- a/taiga/projects/attachments/api.py
+++ b/taiga/projects/attachments/api.py
@@ -25,6 +25,7 @@ from django.contrib.contenttypes.models import ContentType
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base.api import ModelCrudViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.projects.notifications.mixins import WatchedResourceMixin
@@ -35,7 +36,9 @@ from . import serializers
from . import models
-class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
+class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
+ BlockedByProjectMixin, ModelCrudViewSet):
+
model = models.Attachment
serializer_class = serializers.AttachmentSerializer
filter_fields = ["project", "object_id"]
diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py
index 2d929ec7..5a7d8765 100644
--- a/taiga/projects/choices.py
+++ b/taiga/projects/choices.py
@@ -24,3 +24,10 @@ VIDEOCONFERENCES_CHOICES = (
("custom", _("Custom")),
("talky", _("Talky")),
)
+
+BLOCKED_BY_STAFF = "blocked-by-staff"
+BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving"
+BLOCKING_CODES = [
+ (BLOCKED_BY_STAFF, _("This project was blocked by staff")),
+ (BLOCKED_BY_OWNER_LEAVING, _("This project was because the owner left"))
+]
diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py
index 3224d617..a11d6e31 100644
--- a/taiga/projects/custom_attributes/api.py
+++ b/taiga/projects/custom_attributes/api.py
@@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelUpdateRetrieveViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base import exceptions as exc
from taiga.base import filters
from taiga.base import response
@@ -38,7 +39,7 @@ from . import services
# Custom Attribute ViewSets
#######################################################
-class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
+class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer
permission_classes = (permissions.UserStoryCustomAttributePermission,)
@@ -49,7 +50,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order
-class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
+class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.TaskCustomAttribute
serializer_class = serializers.TaskCustomAttributeSerializer
permission_classes = (permissions.TaskCustomAttributePermission,)
@@ -60,7 +61,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
bulk_update_order_action = services.bulk_update_task_custom_attribute_order
-class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
+class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.IssueCustomAttribute
serializer_class = serializers.IssueCustomAttributeSerializer
permission_classes = (permissions.IssueCustomAttributePermission,)
@@ -76,7 +77,7 @@ class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
#######################################################
class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- ModelUpdateRetrieveViewSet):
+ BlockedByProjectMixin, ModelUpdateRetrieveViewSet):
def get_object_for_snapshot(self, obj):
return getattr(obj, self.content_object)
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index 59583ee5..0a93250b 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -24,6 +24,7 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.decorators import detail_route, list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.users.models import User
@@ -43,7 +44,7 @@ from . import serializers
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- ModelCrudViewSet):
+ BlockedByProjectMixin, ModelCrudViewSet):
queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend,
@@ -157,8 +158,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
super().pre_save(obj)
def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
if obj.milestone and obj.milestone.project != obj.project:
raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
"to this issue."))
@@ -179,6 +178,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
raise exc.PermissionDenied(_("You don't have permissions to set this type "
"to this issue."))
+ super().pre_conditions_on_save(obj)
+
@list_route(methods=["GET"])
def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None)
@@ -232,6 +233,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
data = serializer.data
project = Project.objects.get(pk=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
issues = services.create_issues_in_bulk(
data["bulk_issues"], project=project, owner=request.user,
status=project.default_issue_status, severity=project.default_severity,
diff --git a/taiga/projects/likes/mixins/viewsets.py b/taiga/projects/likes/mixins/viewsets.py
index 0b1b1831..03bf8987 100644
--- a/taiga/projects/likes/mixins/viewsets.py
+++ b/taiga/projects/likes/mixins/viewsets.py
@@ -27,10 +27,15 @@ from taiga.projects.likes import services
class LikedResourceMixin:
+ """
+ NOTE:the classes using this mixing must have a method:
+ def pre_conditions_on_save(self, obj)
+ """
@detail_route(methods=["POST"])
def like(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "like", obj)
+ self.pre_conditions_on_save(obj)
services.add_like(obj, user=request.user)
return response.Ok()
@@ -39,6 +44,7 @@ class LikedResourceMixin:
def unlike(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "unlike", obj)
+ self.pre_conditions_on_save(obj)
services.remove_like(obj, user=request.user)
return response.Ok()
diff --git a/taiga/projects/migrations/0035_project_blocked_code.py b/taiga/projects/migrations/0035_project_blocked_code.py
new file mode 100644
index 00000000..809e5cef
--- /dev/null
+++ b/taiga/projects/migrations/0035_project_blocked_code.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0034_project_looking_for_people_note'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='project',
+ name='blocked_code',
+ field=models.CharField(choices=[('blocked-by-staff', 'This project was blocked by staff'), ('blocked-by-owner-leaving', 'This project was because the owner left')], null=True, default=None, max_length=255, blank=True, verbose_name='blocked code'),
+ ),
+ ]
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index 2823f762..b4d54023 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -22,6 +22,7 @@ from taiga.base import filters
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.db import get_object_or_none
@@ -37,7 +38,8 @@ from . import permissions
import datetime
-class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
+class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
+ BlockedByProjectMixin, ModelCrudViewSet):
serializer_class = serializers.MilestoneSerializer
permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,)
diff --git a/taiga/projects/mixins/ordering.py b/taiga/projects/mixins/ordering.py
index 0a98c0a9..6917f1e3 100644
--- a/taiga/projects/mixins/ordering.py
+++ b/taiga/projects/mixins/ordering.py
@@ -54,6 +54,8 @@ class BulkUpdateOrderMixin:
project = get_object_or_404(Project, id=project_id)
self.check_permissions(request, 'bulk_update_order', project)
-
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
self.__class__.bulk_update_order_action(project, request.user, bulk_data)
return response.NoContent(data=None)
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index b5576a3a..60682a32 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -23,6 +23,7 @@ import uuid
from unidecode import unidecode
+from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import signals, Q
@@ -262,6 +263,10 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
total_activity_last_year = models.PositiveIntegerField(null=False, blank=False, default=0,
verbose_name=_("activity last year"), db_index=True)
+ blocked_code = models.CharField(null=True, blank=True, max_length=255,
+ choices=choices.BLOCKING_CODES + settings.EXTRA_BLOCKING_CODES, default=None,
+ verbose_name=_("blocked code"))
+
_cached_user_stories = None
_importing = None
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 726e639b..4fa143a4 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -45,9 +45,13 @@ class WatchedResourceMixin:
Rest Framework resource mixin for resources susceptible
to be notifiable about their changes.
- NOTE: this mixin has hard dependency on HistoryMixin
+ NOTE:
+ - this mixin has hard dependency on HistoryMixin
defined on history app and should be located always
after it on inheritance definition.
+
+ - the classes using this mixing must have a method:
+ def pre_conditions_on_save(self, obj)
"""
_not_notify = False
@@ -64,6 +68,7 @@ class WatchedResourceMixin:
def watch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "watch", obj)
+ self.pre_conditions_on_save(obj)
services.add_watcher(obj, request.user)
return response.Ok()
@@ -71,6 +76,7 @@ class WatchedResourceMixin:
def unwatch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "unwatch", obj)
+ self.pre_conditions_on_save(obj)
services.remove_watcher(obj, request.user)
return response.Ok()
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index a313b8a6..70875cd4 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -325,7 +325,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
class Meta:
model = models.Project
- read_only_fields = ("created_date", "modified_date", "owner", "slug")
+ read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
@@ -402,7 +402,7 @@ class ProjectDetailSerializer(ProjectSerializer):
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
class Meta:
model = models.Project
- read_only_fields = ("created_date", "modified_date", "owner", "slug")
+ read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 8d4cf373..6bb2a1a3 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -22,6 +22,7 @@ from taiga.base import filters, response
from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.projects.models import Project, TaskStatus
from django.http import HttpResponse
@@ -38,7 +39,7 @@ from . import services
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- ModelCrudViewSet):
+ BlockedByProjectMixin, ModelCrudViewSet):
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
@@ -95,7 +96,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
"assigned_to",
"status",
"project")
-
+
return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj):
@@ -147,6 +148,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
data = serializer.data
project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
tasks = services.create_tasks_in_bulk(
data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"],
status_id=data.get("status_id") or project.default_task_status_id,
@@ -166,6 +170,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
services.update_tasks_order_in_bulk(data["bulk_tasks"],
project=project,
field=order_field)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 3a1650ea..815560c1 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -29,6 +29,7 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base import status
from taiga.base.decorators import list_route
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.utils import get_object_or_404
@@ -46,7 +47,7 @@ from . import services
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- ModelCrudViewSet):
+ BlockedByProjectMixin, ModelCrudViewSet):
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend,
@@ -213,6 +214,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
data = serializer.data
project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
user_stories = services.create_userstories_in_bulk(
data["bulk_stories"], project=project, owner=request.user,
status_id=data.get("status_id") or project.default_us_status_id,
@@ -230,6 +234,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
services.update_userstories_order_in_bulk(data["bulk_stories"],
project=project,
field=order_field)
diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py
index fb33304b..2fce2d60 100644
--- a/taiga/projects/votes/mixins/viewsets.py
+++ b/taiga/projects/votes/mixins/viewsets.py
@@ -28,10 +28,15 @@ from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_i
class VotedResourceMixin:
- # Note: Update get_queryset method:
- # def get_queryset(self):
- # qs = super().get_queryset()
- # return self.attach_votes_attrs_to_queryset(qs)
+ """
+ Note: Update get_queryset method:
+ def get_queryset(self):
+ qs = super().get_queryset()
+ return self.attach_votes_attrs_to_queryset(qs)
+
+ - the classes using this mixing must have a method:
+ def pre_conditions_on_save(self, obj)
+ """
def attach_votes_attrs_to_queryset(self, queryset):
qs = attach_total_voters_to_queryset(queryset)
@@ -45,6 +50,7 @@ class VotedResourceMixin:
def upvote(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "upvote", obj)
+ self.pre_conditions_on_save(obj)
services.add_vote(obj, user=request.user)
return response.Ok()
@@ -53,6 +59,7 @@ class VotedResourceMixin:
def downvote(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "downvote", obj)
+ self.pre_conditions_on_save(obj)
services.remove_vote(obj, user=request.user)
return response.Ok()
diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py
index a88c8d14..ea014233 100644
--- a/taiga/projects/wiki/api.py
+++ b/taiga/projects/wiki/api.py
@@ -23,6 +23,7 @@ from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route
from taiga.projects.models import Project
@@ -38,7 +39,9 @@ from . import permissions
from . import serializers
-class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
+class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
+ BlockedByProjectMixin, ModelCrudViewSet):
+
model = models.WikiPage
serializer_class = serializers.WikiPageSerializer
permission_classes = (permissions.WikiPagePermission,)
@@ -89,7 +92,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
resource_model = models.WikiPage
-class WikiLinkViewSet(ModelCrudViewSet):
+class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer
permission_classes = (permissions.WikiLinkPermission,)
diff --git a/taiga/users/api.py b/taiga/users/api.py
index f5d59bd4..96e2742d 100644
--- a/taiga/users/api.py
+++ b/taiga/users/api.py
@@ -31,6 +31,7 @@ from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend
@@ -403,7 +404,7 @@ class UsersViewSet(ModelCrudViewSet):
## Role
######################################################
-class RolesViewSet(ModelCrudViewSet):
+class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Role
serializer_class = serializers.RoleSerializer
permission_classes = (permissions.RolesPermission, )
diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py
index b9092f47..a9b8545e 100644
--- a/taiga/webhooks/api.py
+++ b/taiga/webhooks/api.py
@@ -15,10 +15,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.utils.translation import ugettext as _
+
from taiga.base import filters
from taiga.base import response
+from taiga.base import exceptions as exc
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.decorators import detail_route
@@ -28,7 +32,7 @@ from . import permissions
from . import tasks
-class WebhookViewSet(ModelCrudViewSet):
+class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Webhook
serializer_class = serializers.WebhookSerializer
permission_classes = (permissions.WebhookPermission,)
@@ -39,6 +43,7 @@ class WebhookViewSet(ModelCrudViewSet):
def test(self, request, pk=None):
webhook = self.get_object()
self.check_permissions(request, 'test', webhook)
+ self.pre_conditions_blocked(webhook)
webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key)
log = serializers.WebhookLogSerializer(webhooklog)
@@ -57,8 +62,9 @@ class WebhookLogViewSet(ModelListViewSet):
def resend(self, request, pk=None):
webhooklog = self.get_object()
self.check_permissions(request, 'resend', webhooklog)
-
webhook = webhooklog.webhook
+ if webhook.project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
webhooklog = tasks.resend_webhook(webhook.id, webhook.url, webhook.key,
webhooklog.request_data)
diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index 520e266a..f6cc6339 100644
--- a/tests/integration/resources_permissions/test_attachment_resources.py
+++ b/tests/integration/resources_permissions/test_attachment_resources.py
@@ -5,6 +5,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.projects import choices as project_choices
from taiga.projects.attachments.serializers import AttachmentSerializer
from tests import factories as f
@@ -47,6 +48,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -68,6 +74,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -81,6 +95,9 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
return m
@@ -96,6 +113,9 @@ def data_us(data):
m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9)
m.private_user_story2_attachment = f.UserStoryAttachmentFactory(project=data.private_project2,
content_object=m.private_user_story2)
+ m.blocked_user_story = f.UserStoryFactory(project=data.blocked_project, ref=13)
+ m.blocked_user_story_attachment = f.UserStoryAttachmentFactory(project=data.blocked_project,
+ content_object=m.blocked_user_story)
return m
@@ -108,6 +128,8 @@ def data_task(data):
m.private_task1_attachment = f.TaskAttachmentFactory(project=data.private_project1, content_object=m.private_task1)
m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10)
m.private_task2_attachment = f.TaskAttachmentFactory(project=data.private_project2, content_object=m.private_task2)
+ m.blocked_task = f.TaskFactory(project=data.blocked_project, ref=14)
+ m.blocked_task_attachment = f.TaskAttachmentFactory(project=data.blocked_project, content_object=m.blocked_task)
return m
@@ -120,6 +142,8 @@ def data_issue(data):
m.private_issue1_attachment = f.IssueAttachmentFactory(project=data.private_project1, content_object=m.private_issue1)
m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11)
m.private_issue2_attachment = f.IssueAttachmentFactory(project=data.private_project2, content_object=m.private_issue2)
+ m.blocked_issue = f.IssueFactory(project=data.blocked_project, ref=11)
+ m.blocked_issue_attachment = f.IssueAttachmentFactory(project=data.blocked_project, content_object=m.blocked_issue)
return m
@@ -132,6 +156,8 @@ def data_wiki(data):
m.private_wiki1_attachment = f.WikiAttachmentFactory(project=data.private_project1, content_object=m.private_wiki1)
m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12)
m.private_wiki2_attachment = f.WikiAttachmentFactory(project=data.private_project2, content_object=m.private_wiki2)
+ m.blocked_wiki = f.WikiPageFactory(project=data.blocked_project, slug=1)
+ m.blocked_wiki_attachment = f.WikiAttachmentFactory(project=data.blocked_project, content_object=m.blocked_wiki)
return m
@@ -139,6 +165,7 @@ def test_user_story_attachment_retrieve(client, data, data_us):
public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk})
private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk})
private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk})
+ blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk})
users = [
None,
@@ -154,12 +181,15 @@ def test_user_story_attachment_retrieve(client, data, data_us):
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_task_attachment_retrieve(client, data, data_task):
public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk})
private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk})
private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk})
+ blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk})
users = [
None,
@@ -175,12 +205,15 @@ def test_task_attachment_retrieve(client, data, data_task):
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_issue_attachment_retrieve(client, data, data_issue):
public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk})
private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk})
private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk})
+ blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk})
users = [
None,
@@ -196,12 +229,15 @@ def test_issue_attachment_retrieve(client, data, data_issue):
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_attachment_retrieve(client, data, data_wiki):
public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk})
private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk})
private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk})
+ blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk})
users = [
None,
@@ -217,6 +253,8 @@ def test_wiki_attachment_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]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_user_story_attachment_update(client, data, data_us):
@@ -226,7 +264,8 @@ def test_user_story_attachment_update(client, data, data_us):
args=[data_us.private_user_story1_attachment.pk])
private_url2 = reverse("userstory-attachments-detail",
args=[data_us.private_user_story2_attachment.pk])
-
+ blocked_url = reverse("userstory-attachments-detail",
+ args=[data_us.blocked_user_story_attachment.pk])
users = [
None,
data.registered_user,
@@ -252,11 +291,16 @@ def test_user_story_attachment_update(client, data, data_us):
# assert results == [401, 403, 403, 400, 400]
assert results == [405, 405, 405, 405, 405]
+ results = helper_test_http_method(client, "put", blocked_url, attachment_data, users)
+ # assert results == [401, 403, 403, 400, 400]
+ assert results == [405, 405, 405, 405, 405]
+
def test_task_attachment_update(client, data, data_task):
public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk})
private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk})
private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk})
+ blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk})
users = [
None,
@@ -279,12 +323,16 @@ def test_task_attachment_update(client, data, data_task):
results = helper_test_http_method(client, 'put', private_url2, attachment_data, users)
assert results == [405, 405, 405, 405, 405]
# assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users)
+ assert results == [405, 405, 405, 405, 405]
+ # assert results == [401, 403, 403, 200, 200]
def test_issue_attachment_update(client, data, data_issue):
public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk})
private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk})
private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk})
+ blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk})
users = [
None,
@@ -307,12 +355,16 @@ def test_issue_attachment_update(client, data, data_issue):
results = helper_test_http_method(client, 'put', private_url2, attachment_data, users)
assert results == [405, 405, 405, 405, 405]
# assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users)
+ assert results == [405, 405, 405, 405, 405]
+ # assert results == [401, 403, 403, 200, 200]
def test_wiki_attachment_update(client, data, data_wiki):
public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk})
private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk})
private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk})
+ blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk})
users = [
None,
@@ -335,12 +387,16 @@ def test_wiki_attachment_update(client, data, data_wiki):
results = helper_test_http_method(client, 'put', private_url2, attachment_data, users)
assert results == [405, 405, 405, 405, 405]
# assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users)
+ assert results == [405, 405, 405, 405, 405]
+ # assert results == [401, 403, 403, 200, 200]
def test_user_story_attachment_patch(client, data, data_us):
public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk})
private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk})
private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk})
+ blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk})
users = [
None,
@@ -359,12 +415,15 @@ def test_user_story_attachment_patch(client, data, data_us):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_task_attachment_patch(client, data, data_task):
public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk})
private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk})
private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk})
+ blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk})
users = [
None,
@@ -383,12 +442,15 @@ def test_task_attachment_patch(client, data, data_task):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_issue_attachment_patch(client, data, data_issue):
public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk})
private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk})
private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk})
+ blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk})
users = [
None,
@@ -407,12 +469,15 @@ def test_issue_attachment_patch(client, data, data_issue):
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_wiki_attachment_patch(client, data, data_wiki):
public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk})
private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk})
private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk})
+ blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk})
users = [
None,
@@ -431,12 +496,15 @@ def test_wiki_attachment_patch(client, data, data_wiki):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users)
+ assert results == [401, 403, 403, 451, 451]
def test_user_story_attachment_delete(client, data, data_us):
public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk})
private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk})
private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk})
+ blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk})
users = [
None,
@@ -451,12 +519,15 @@ def test_user_story_attachment_delete(client, data, data_us):
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_attachment_delete(client, data, data_task):
public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk})
private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk})
private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk})
+ blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk})
users = [
None,
@@ -471,12 +542,15 @@ def test_task_attachment_delete(client, data, data_task):
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_attachment_delete(client, data, data_issue):
public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk})
private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk})
private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk})
+ blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk})
users = [
None,
@@ -491,12 +565,15 @@ def test_issue_attachment_delete(client, data, data_issue):
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_wiki_attachment_delete(client, data, data_wiki):
public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk})
private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk})
private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk})
+ blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk})
users = [
None,
@@ -511,6 +588,8 @@ def test_wiki_attachment_delete(client, data, data_wiki):
assert results == [401, 204]
results = helper_test_http_method(client, 'delete', private_url2, None, users)
assert results == [401, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 451]
def test_user_story_attachment_create(client, data, data_us):
@@ -536,6 +615,15 @@ def test_user_story_attachment_create(client, data, data_us):
after_each_request=_after_each_request_hook)
assert results == [401, 403, 403, 201, 201]
+ attachment_data = {"description": "test",
+ "object_id": data_us.blocked_user_story_attachment.object_id,
+ "project": data_us.blocked_user_story_attachment.project_id,
+ "attached_file": SimpleUploadedFile("test.txt", b"test")}
+ results = helper_test_http_method(client, 'post', url, attachment_data, users,
+ content_type=MULTIPART_CONTENT,
+ after_each_request=_after_each_request_hook)
+ assert results == [401, 403, 403, 451, 451]
+
def test_task_attachment_create(client, data, data_task):
url = reverse('task-attachments-list')
@@ -560,6 +648,18 @@ def test_task_attachment_create(client, data, data_task):
after_each_request=_after_each_request_hook)
assert results == [401, 403, 403, 201, 201]
+ attachment_data = {"description": "test",
+ "object_id": data_task.blocked_task_attachment.object_id,
+ "project": data_task.blocked_task_attachment.project_id,
+ "attached_file": SimpleUploadedFile("test.txt", b"test")}
+
+ _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0)
+
+ results = helper_test_http_method(client, 'post', url, attachment_data, users,
+ content_type=MULTIPART_CONTENT,
+ after_each_request=_after_each_request_hook)
+ assert results == [401, 403, 403, 451, 451]
+
def test_issue_attachment_create(client, data, data_issue):
url = reverse('issue-attachments-list')
@@ -585,6 +685,19 @@ def test_issue_attachment_create(client, data, data_issue):
assert results == [401, 403, 403, 201, 201]
+ attachment_data = {"description": "test",
+ "object_id": data_issue.blocked_issue_attachment.object_id,
+ "project": data_issue.blocked_issue_attachment.project_id,
+ "attached_file": SimpleUploadedFile("test.txt", b"test")}
+
+ _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0)
+
+ results = helper_test_http_method(client, 'post', url, attachment_data, users,
+ content_type=MULTIPART_CONTENT,
+ after_each_request=_after_each_request_hook)
+
+ assert results == [401, 403, 403, 451, 451]
+
def test_wiki_attachment_create(client, data, data_wiki):
url = reverse('wiki-attachments-list')
@@ -610,6 +723,19 @@ def test_wiki_attachment_create(client, data, data_wiki):
assert results == [401, 201, 201, 201, 201]
+ attachment_data = {"description": "test",
+ "object_id": data_wiki.blocked_wiki_attachment.object_id,
+ "project": data_wiki.blocked_wiki_attachment.project_id,
+ "attached_file": SimpleUploadedFile("test.txt", b"test")}
+
+ _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0)
+
+ results = helper_test_http_method(client, 'post', url, attachment_data, users,
+ content_type=MULTIPART_CONTENT,
+ after_each_request=_after_each_request_hook)
+
+ assert results == [401, 403, 403, 451, 451]
+
def test_user_story_attachment_list(client, data, data_us):
url = reverse('userstory-attachments-list')
@@ -623,7 +749,7 @@ def test_user_story_attachment_list(client, data, data_us):
]
results = helper_test_http_method_and_count(client, 'get', url, None, users)
- assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)]
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)]
def test_task_attachment_list(client, data, data_task):
@@ -638,7 +764,7 @@ def test_task_attachment_list(client, data, data_task):
]
results = helper_test_http_method_and_count(client, 'get', url, None, users)
- assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)]
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)]
def test_issue_attachment_list(client, data, data_issue):
@@ -653,7 +779,7 @@ def test_issue_attachment_list(client, data, data_issue):
]
results = helper_test_http_method_and_count(client, 'get', url, None, users)
- assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)]
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)]
def test_wiki_attachment_list(client, data, data_wiki):
@@ -668,4 +794,4 @@ def test_wiki_attachment_list(client, data, data_wiki):
]
results = helper_test_http_method_and_count(client, 'get', url, None, users)
- assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)]
+ assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)]
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 da0fa833..81cfdb31 100644
--- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
@@ -19,6 +19,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,
ANON_PERMISSIONS, USER_PERMISSIONS)
@@ -52,6 +53,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -81,6 +87,17 @@ def data():
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ email=m.project_member_with_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ email=m.project_member_without_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=[])
+
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
@@ -93,9 +110,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project)
m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1)
m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2)
+ m.blocked_issue_ca = f.IssueCustomAttributeFactory(project=m.blocked_project)
m.public_issue = f.IssueFactory(project=m.public_project,
status__project=m.public_project,
@@ -115,10 +137,17 @@ def data():
priority__project=m.private_project2,
type__project=m.private_project2,
milestone__project=m.private_project2)
+ m.blocked_issue = f.IssueFactory(project=m.blocked_project,
+ status__project=m.blocked_project,
+ severity__project=m.blocked_project,
+ priority__project=m.blocked_project,
+ type__project=m.blocked_project,
+ milestone__project=m.blocked_project)
m.public_issue_cav = m.public_issue.custom_attributes_values
m.private_issue_cav1 = m.private_issue1.custom_attributes_values
m.private_issue_cav2 = m.private_issue2.custom_attributes_values
+ m.blocked_issue_cav = m.blocked_issue.custom_attributes_values
return m
@@ -131,6 +160,7 @@ def test_issue_custom_attribute_retrieve(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
+ blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk})
users = [
None,
@@ -146,12 +176,15 @@ def test_issue_custom_attribute_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_issue_custom_attribute_create(client, data):
public_url = reverse('issue-custom-attributes-list')
private1_url = reverse('issue-custom-attributes-list')
private2_url = reverse('issue-custom-attributes-list')
+ blocked_url = reverse('issue-custom-attributes-list')
users = [
None,
@@ -176,11 +209,17 @@ def test_issue_custom_attribute_create(client, data):
results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 201]
+ issue_ca_data = {"name": "test-new", "project": data.blocked_project.id}
+ issue_ca_data = json.dumps(issue_ca_data)
+ results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_custom_attribute_update(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
+ blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk})
users = [
None,
@@ -208,11 +247,18 @@ def test_issue_custom_attribute_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 200]
+ issue_ca_data = serializers.IssueCustomAttributeSerializer(data.blocked_issue_ca).data
+ issue_ca_data["name"] = "test"
+ issue_ca_data = json.dumps(issue_ca_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_custom_attribute_delete(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
+ blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk})
users = [
None,
@@ -228,6 +274,8 @@ def test_issue_custom_attribute_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_custom_attribute_list(client, data):
@@ -249,12 +297,12 @@ def test_issue_custom_attribute_list(client, data):
client.login(data.project_member_with_perms)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
@@ -262,6 +310,7 @@ def test_issue_custom_attribute_patch(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
+ blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk})
users = [
None,
@@ -277,6 +326,8 @@ def test_issue_custom_attribute_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_custom_attribute_action_bulk_update_order(client, data):
@@ -311,6 +362,12 @@ def test_issue_custom_attribute_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_issue_custom_attributes": [(1,2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
#########################################################
# Issue Custom Attribute
@@ -321,6 +378,7 @@ def test_issue_custom_attributes_values_retrieve(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
+ blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk})
users = [
None,
@@ -336,12 +394,15 @@ def test_issue_custom_attributes_values_retrieve(client, data):
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_issue_custom_attributes_values_update(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
+ blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk})
users = [
None,
@@ -369,11 +430,18 @@ def test_issue_custom_attributes_values_update(client, data):
results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
assert results == [401, 403, 403, 200, 200]
+ issue_data = serializers.IssueCustomAttributesValuesSerializer(data.blocked_issue_cav).data
+ issue_data["attributes_values"] = {str(data.blocked_issue_ca.pk): "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_custom_attributes_values_patch(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
+ blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk})
users = [
None,
@@ -397,3 +465,8 @@ def test_issue_custom_attributes_values_patch(client, data):
"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({"attributes_values": {str(data.blocked_issue_ca.pk): "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]
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 469efacc..11d3bf62 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -2,6 +2,7 @@ import uuid
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.base.utils import json
@@ -51,6 +52,12 @@ def data():
public_permissions=[],
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ issues_csv_uuid=uuid.uuid4().hex,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -72,6 +79,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -85,6 +100,10 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_issue = f.IssueFactory(project=m.public_project,
status__project=m.public_project,
severity__project=m.public_project,
@@ -103,6 +122,12 @@ def data():
priority__project=m.private_project2,
type__project=m.private_project2,
milestone__project=m.private_project2)
+ m.blocked_issue = f.IssueFactory(project=m.blocked_project,
+ status__project=m.blocked_project,
+ severity__project=m.blocked_project,
+ priority__project=m.blocked_project,
+ type__project=m.blocked_project,
+ milestone__project=m.blocked_project)
return m
@@ -111,6 +136,7 @@ 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})
private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -126,12 +152,15 @@ def test_issue_retrieve(client, data):
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_issue_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})
+ blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -160,6 +189,12 @@ def test_issue_update(client, 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]
+
def test_issue_update_with_project_change(client):
user1 = f.UserFactory.create()
@@ -278,6 +313,7 @@ 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,
@@ -292,6 +328,8 @@ def test_issue_delete(client, data):
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):
@@ -313,14 +351,14 @@ def test_issue_list(client, data):
response = client.get(url)
issues_data = json.loads(response.content.decode('utf-8'))
- assert len(issues_data) == 3
+ 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) == 3
+ assert len(issues_data) == 4
assert response.status_code == 200
@@ -390,11 +428,24 @@ def test_issue_create(client, data):
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):
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,
@@ -417,6 +468,10 @@ def test_issue_patch(client, data):
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]
+
def test_issue_bulk_create(client, data):
data.public_issue.project.default_issue_status = f.IssueStatusFactory()
@@ -437,6 +492,12 @@ def test_issue_bulk_create(client, data):
data.private_issue2.project.default_severity = f.SeverityFactory()
data.private_issue2.project.save()
+ data.blocked_issue.project.default_issue_status = f.IssueStatusFactory()
+ data.blocked_issue.project.default_issue_type = f.IssueTypeFactory()
+ data.blocked_issue.project.default_priority = f.PriorityFactory()
+ data.blocked_issue.project.default_severity = f.SeverityFactory()
+ data.blocked_issue.project.save()
+
url = reverse('issues-bulk-create')
users = [
@@ -462,11 +523,17 @@ def test_issue_bulk_create(client, data):
results = helper_test_http_method(client, 'post', url, bulk_data, users)
assert results == [401, 403, 403, 200, 200]
+ bulk_data = json.dumps({"bulk_issues": "test1\ntest2",
+ "project_id": data.blocked_issue.project.pk})
+ results = helper_test_http_method(client, 'post', url, bulk_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_issue_action_upvote(client, data):
public_url = reverse('issues-upvote', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-upvote', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-upvote', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-upvote', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -482,12 +549,15 @@ def test_issue_action_upvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_issue_action_downvote(client, data):
public_url = reverse('issues-downvote', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-downvote', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-downvote', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-downvote', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -503,12 +573,15 @@ def test_issue_action_downvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_issue_voters_list(client, data):
public_url = reverse('issue-voters-list', kwargs={"resource_id": data.public_issue.pk})
private_url1 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue1.pk})
private_url2 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue2.pk})
+ blocked_url = reverse('issue-voters-list', kwargs={"resource_id": data.blocked_issue.pk})
users = [
None,
@@ -524,6 +597,8 @@ def test_issue_voters_list(client, data):
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_issue_voters_retrieve(client, data):
@@ -536,6 +611,9 @@ def test_issue_voters_retrieve(client, data):
add_vote(data.private_issue2, data.project_owner)
private_url2 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue2.pk,
"pk": data.project_owner.pk})
+ add_vote(data.blocked_issue, data.project_owner)
+ blocked_url = reverse('issue-voters-detail', kwargs={"resource_id": data.blocked_issue.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -551,13 +629,16 @@ def test_issue_voters_retrieve(client, data):
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_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_project1.issues_csv_uuid
+ csv_private2_uuid = data.private_project2.issues_csv_uuid
+ csv_blocked_uuid = data.blocked_project.issues_csv_uuid
users = [
None,
@@ -576,11 +657,15 @@ def test_issues_csv(client, data):
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})
private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-watch', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -596,12 +681,15 @@ def test_issue_action_watch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_issue_action_unwatch(client, data):
public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.pk})
+ blocked_url = reverse('issues-unwatch', kwargs={"pk": data.blocked_issue.pk})
users = [
None,
@@ -617,12 +705,15 @@ def test_issue_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_issue_watchers_list(client, data):
public_url = reverse('issue-watchers-list', kwargs={"resource_id": data.public_issue.pk})
private_url1 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue1.pk})
private_url2 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue2.pk})
+ blocked_url = reverse('issue-watchers-list', kwargs={"resource_id": data.blocked_issue.pk})
users = [
None,
@@ -638,6 +729,8 @@ def test_issue_watchers_list(client, data):
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_issue_watchers_retrieve(client, data):
@@ -650,7 +743,9 @@ def test_issue_watchers_retrieve(client, data):
add_watcher(data.private_issue2, data.project_owner)
private_url2 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue2.pk,
"pk": data.project_owner.pk})
-
+ add_watcher(data.blocked_issue, data.project_owner)
+ blocked_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.blocked_issue.pk,
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
@@ -665,3 +760,5 @@ def test_issue_watchers_retrieve(client, data):
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]
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index 40a8c008..b9777ce1 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -1,6 +1,8 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+
+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
@@ -43,6 +45,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -64,6 +71,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -77,9 +92,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_milestone = f.MilestoneFactory(project=m.public_project)
m.private_milestone1 = f.MilestoneFactory(project=m.private_project1)
m.private_milestone2 = f.MilestoneFactory(project=m.private_project2)
+ m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project)
return m
@@ -88,6 +108,7 @@ def test_milestone_retrieve(client, data):
public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -103,12 +124,15 @@ def test_milestone_retrieve(client, data):
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_milestone_update(client, data):
public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -136,11 +160,18 @@ def test_milestone_update(client, data):
results = helper_test_http_method(client, 'put', private_url2, milestone_data, users)
assert results == [401, 403, 403, 200, 200]
+ milestone_data = MilestoneSerializer(data.blocked_milestone).data
+ milestone_data["name"] = "test"
+ milestone_data = json.dumps(milestone_data)
+ results = helper_test_http_method(client, 'put', blocked_url, milestone_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_milestone_delete(client, data):
public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -154,6 +185,8 @@ def test_milestone_delete(client, data):
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_milestone_list(client, data):
@@ -175,14 +208,14 @@ def test_milestone_list(client, data):
response = client.get(url)
milestones_data = json.loads(response.content.decode('utf-8'))
- assert len(milestones_data) == 3
+ assert len(milestones_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
milestones_data = json.loads(response.content.decode('utf-8'))
- assert len(milestones_data) == 3
+ assert len(milestones_data) == 4
assert response.status_code == 200
@@ -227,11 +260,21 @@ def test_milestone_create(client, data):
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete())
assert results == [401, 403, 403, 201, 201]
+ create_data = json.dumps({
+ "name": "test",
+ "estimated_start": "2014-12-10",
+ "estimated_finish": "2014-12-24",
+ "project": data.blocked_project.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete())
+ assert results == [401, 403, 403, 451, 451]
+
def test_milestone_patch(client, data):
public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -253,11 +296,16 @@ def test_milestone_patch(client, data):
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]
+ patch_data = json.dumps({"name": "test"})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_milestone_action_stats(client, data):
public_url = reverse('milestones-stats', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-stats', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-stats', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-stats', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -276,11 +324,15 @@ def test_milestone_action_stats(client, data):
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_milestone_action_watch(client, data):
public_url = reverse('milestones-watch', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-watch', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-watch', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-watch', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -296,12 +348,15 @@ def test_milestone_action_watch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_milestone_action_unwatch(client, data):
public_url = reverse('milestones-unwatch', kwargs={"pk": data.public_milestone.pk})
private_url1 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone1.pk})
private_url2 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone2.pk})
+ blocked_url = reverse('milestones-unwatch', kwargs={"pk": data.blocked_milestone.pk})
users = [
None,
@@ -317,12 +372,15 @@ def test_milestone_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_milestone_watchers_list(client, data):
public_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.public_milestone.pk})
private_url1 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone1.pk})
private_url2 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone2.pk})
+ blocked_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.blocked_milestone.pk})
users = [
None,
@@ -338,6 +396,8 @@ def test_milestone_watchers_list(client, data):
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_milestone_watchers_retrieve(client, data):
@@ -350,6 +410,9 @@ def test_milestone_watchers_retrieve(client, data):
add_watcher(data.private_milestone2, data.project_owner)
private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk,
"pk": data.project_owner.pk})
+ add_watcher(data.blocked_milestone, data.project_owner)
+ blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -365,3 +428,5 @@ def test_milestone_watchers_retrieve(client, data):
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]
diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py
new file mode 100644
index 00000000..1f77d055
--- /dev/null
+++ b/tests/integration/resources_permissions/test_modules_resources.py
@@ -0,0 +1,209 @@
+import uuid
+
+from django.core.urlresolvers import reverse
+
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from taiga.base.utils import json
+
+from tests import factories as f
+from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
+from taiga.projects import choices as project_choices
+from taiga.projects.votes.services import add_vote
+from taiga.projects.notifications.services import add_watcher
+from taiga.projects.occ import OCCResourceMixin
+
+from unittest import mock
+
+import pytest
+pytestmark = pytest.mark.django_db
+
+
+def setup_module(module):
+ disconnect_signals()
+
+
+def teardown_module(module):
+ reconnect_signals()
+
+
+@pytest.fixture
+def data():
+ m = type("Models", (object,), {})
+
+ m.registered_user = f.UserFactory.create()
+ m.project_member_with_perms = f.UserFactory.create()
+ m.project_member_without_perms = f.UserFactory.create()
+ m.project_owner = f.UserFactory.create()
+ m.other_user = f.UserFactory.create()
+
+ m.public_project = f.ProjectFactory(is_private=False,
+ anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
+ public_permissions=list(map(lambda x: x[0], USER_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)),
+ owner=m.project_owner)
+ m.private_project2 = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
+
+ m.public_membership = f.MembershipFactory(project=m.public_project,
+ user=m.project_member_with_perms,
+ role__project=m.public_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.private_membership1 = f.MembershipFactory(project=m.private_project1,
+ user=m.project_member_with_perms,
+ role__project=m.private_project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.private_project1,
+ user=m.project_member_without_perms,
+ role__project=m.private_project1,
+ role__permissions=[])
+ m.private_membership2 = f.MembershipFactory(project=m.private_project2,
+ user=m.project_member_with_perms,
+ role__project=m.private_project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.private_project2,
+ user=m.project_member_without_perms,
+ role__project=m.private_project2,
+ role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
+
+ f.MembershipFactory(project=m.public_project,
+ user=m.project_owner,
+ is_owner=True)
+
+ f.MembershipFactory(project=m.private_project1,
+ user=m.project_owner,
+ is_owner=True)
+
+ f.MembershipFactory(project=m.private_project2,
+ user=m.project_owner,
+ is_owner=True)
+
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
+ return m
+
+
+def test_modules_retrieve(client, data):
+ public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk})
+ private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk})
+ private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.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 == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'get', private_url1, None, users)
+ assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'get', private_url2, None, users)
+ assert results == [404, 404, 404, 403, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [404, 404, 404, 403, 200]
+
+
+def test_modules_update(client, data):
+ public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk})
+ private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk})
+ private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.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"):
+ results = helper_test_http_method(client, 'put', public_url, {"att": "test"}, users)
+ assert results == [405, 405, 405, 405, 405]
+
+ results = helper_test_http_method(client, 'put', private_url1, {"att": "test"}, users)
+ assert results == [405, 405, 405, 405, 405]
+
+ results = helper_test_http_method(client, 'put', private_url2, {"att": "test"}, users)
+ assert results == [405, 405, 405, 405, 405]
+
+ results = helper_test_http_method(client, 'put', blocked_url, {"att": "test"}, users)
+ assert results == [405, 405, 405, 405, 405]
+
+
+def test_modules_delete(client, data):
+ public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk})
+ private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk})
+ private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.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 == [405, 405, 405, 405]
+ results = helper_test_http_method(client, 'delete', private_url1, None, users)
+ assert results == [405, 405, 405, 405]
+ results = helper_test_http_method(client, 'delete', private_url2, None, users)
+ assert results == [405, 405, 405, 405]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [405, 405, 405, 405]
+
+
+def test_modules_patch(client, data):
+ public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk})
+ private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk})
+ private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.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({"att": "test"})
+ results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
+ assert results == [401, 403, 403, 403, 204]
+
+ patch_data = json.dumps({"att": "test"})
+ results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
+ assert results == [401, 403, 403, 403, 204]
+
+ patch_data = json.dumps({"att": "test"})
+ results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
+ assert results == [404, 404, 404, 403, 204]
+
+ patch_data = json.dumps({"att": "test"})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [404, 404, 404, 403, 451]
diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py
index c94ec9cc..1ffcf87a 100644
--- a/tests/integration/resources_permissions/test_projects_choices_resources.py
+++ b/tests/integration/resources_permissions/test_projects_choices_resources.py
@@ -1,6 +1,7 @@
from django.core.urlresolvers import reverse
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
@@ -34,6 +35,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -61,6 +67,14 @@ def data():
email=m.project_member_without_perms.email,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -74,33 +88,44 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
m.private_points2 = f.PointsFactory(project=m.private_project2)
+ m.blocked_points = f.PointsFactory(project=m.blocked_project)
m.public_user_story_status = f.UserStoryStatusFactory(project=m.public_project)
m.private_user_story_status1 = f.UserStoryStatusFactory(project=m.private_project1)
m.private_user_story_status2 = f.UserStoryStatusFactory(project=m.private_project2)
+ m.blocked_user_story_status = f.UserStoryStatusFactory(project=m.blocked_project)
m.public_task_status = f.TaskStatusFactory(project=m.public_project)
m.private_task_status1 = f.TaskStatusFactory(project=m.private_project1)
m.private_task_status2 = f.TaskStatusFactory(project=m.private_project2)
+ m.blocked_task_status = f.TaskStatusFactory(project=m.blocked_project)
m.public_issue_status = f.IssueStatusFactory(project=m.public_project)
m.private_issue_status1 = f.IssueStatusFactory(project=m.private_project1)
m.private_issue_status2 = f.IssueStatusFactory(project=m.private_project2)
+ m.blocked_issue_status = f.IssueStatusFactory(project=m.blocked_project)
m.public_issue_type = f.IssueTypeFactory(project=m.public_project)
m.private_issue_type1 = f.IssueTypeFactory(project=m.private_project1)
m.private_issue_type2 = f.IssueTypeFactory(project=m.private_project2)
+ m.blocked_issue_type = f.IssueTypeFactory(project=m.blocked_project)
m.public_priority = f.PriorityFactory(project=m.public_project)
m.private_priority1 = f.PriorityFactory(project=m.private_project1)
m.private_priority2 = f.PriorityFactory(project=m.private_project2)
+ m.blocked_priority = f.PriorityFactory(project=m.blocked_project)
m.public_severity = f.SeverityFactory(project=m.public_project)
m.private_severity1 = f.SeverityFactory(project=m.private_project1)
m.private_severity2 = f.SeverityFactory(project=m.private_project2)
+ m.blocked_severity = f.SeverityFactory(project=m.blocked_project)
m.project_template = m.public_project.creation_template
@@ -111,6 +136,7 @@ def test_roles_retrieve(client, data):
public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk})
private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk})
private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk})
+ blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk})
users = [
None,
@@ -126,12 +152,15 @@ def test_roles_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_roles_update(client, data):
public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk})
private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk})
private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk})
+ blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk})
users = [
None,
@@ -159,11 +188,18 @@ def test_roles_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, role_data, users)
assert results == [401, 403, 403, 403, 200]
+ role_data = RoleSerializer(data.blocked_project.roles.all()[0]).data
+ role_data["name"] = "test"
+ role_data = json.dumps(role_data)
+ results = helper_test_http_method(client, 'put', blocked_url, role_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_roles_delete(client, data):
public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk})
private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk})
private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk})
+ blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk})
users = [
None,
@@ -179,6 +215,8 @@ def test_roles_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_roles_list(client, data):
@@ -204,13 +242,13 @@ def test_roles_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 5
+ assert len(projects_data) == 7
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 5
+ assert len(projects_data) == 7
assert response.status_code == 200
@@ -218,6 +256,7 @@ def test_roles_patch(client, data):
public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk})
private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk})
private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk})
+ blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk})
users = [
None,
@@ -233,12 +272,15 @@ def test_roles_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_points_retrieve(client, data):
public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk})
private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk})
private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk})
+ blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk})
users = [
None,
@@ -254,12 +296,15 @@ def test_points_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_points_update(client, data):
public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk})
private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk})
private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk})
+ blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk})
users = [
None,
@@ -287,11 +332,18 @@ def test_points_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, points_data, users)
assert results == [401, 403, 403, 403, 200]
+ points_data = serializers.PointsSerializer(data.blocked_points).data
+ points_data["name"] = "test"
+ points_data = json.dumps(points_data)
+ results = helper_test_http_method(client, 'put', blocked_url, points_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_points_delete(client, data):
public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk})
private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk})
private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk})
+ blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk})
users = [
None,
@@ -307,6 +359,8 @@ def test_points_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_points_list(client, data):
@@ -332,13 +386,13 @@ def test_points_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -346,6 +400,7 @@ def test_points_patch(client, data):
public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk})
private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk})
private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk})
+ blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk})
users = [
None,
@@ -361,6 +416,8 @@ def test_points_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_points_action_bulk_update_order(client, data):
@@ -395,11 +452,19 @@ def test_points_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_points": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_user_story_status_retrieve(client, data):
public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk})
private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk})
private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk})
+ blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk})
users = [
None,
@@ -415,12 +480,15 @@ def test_user_story_status_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_user_story_status_update(client, data):
public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk})
private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk})
private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk})
+ blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk})
users = [
None,
@@ -448,11 +516,18 @@ def test_user_story_status_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, user_story_status_data, users)
assert results == [401, 403, 403, 403, 200]
+ user_story_status_data = serializers.UserStoryStatusSerializer(data.blocked_user_story_status).data
+ user_story_status_data["name"] = "test"
+ user_story_status_data = json.dumps(user_story_status_data)
+ results = helper_test_http_method(client, 'put', blocked_url, user_story_status_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_user_story_status_delete(client, data):
public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk})
private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk})
private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk})
+ blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk})
users = [
None,
@@ -468,6 +543,9 @@ def test_user_story_status_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_user_story_status_list(client, data):
@@ -493,13 +571,13 @@ def test_user_story_status_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -507,6 +585,7 @@ def test_user_story_status_patch(client, data):
public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk})
private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk})
private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk})
+ blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk})
users = [
None,
@@ -522,6 +601,8 @@ def test_user_story_status_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_user_story_status_action_bulk_update_order(client, data):
@@ -556,11 +637,19 @@ def test_user_story_status_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_userstory_statuses": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_status_retrieve(client, data):
public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk})
private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk})
private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk})
+ blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk})
users = [
None,
@@ -576,12 +665,15 @@ def test_task_status_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_task_status_update(client, data):
public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk})
private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk})
private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk})
+ blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk})
users = [
None,
@@ -609,11 +701,18 @@ def test_task_status_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, task_status_data, users)
assert results == [401, 403, 403, 403, 200]
+ task_status_data = serializers.TaskStatusSerializer(data.blocked_task_status).data
+ task_status_data["name"] = "test"
+ task_status_data = json.dumps(task_status_data)
+ results = helper_test_http_method(client, 'put', blocked_url, task_status_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_status_delete(client, data):
public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk})
private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk})
private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk})
+ blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk})
users = [
None,
@@ -629,6 +728,9 @@ def test_task_status_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_status_list(client, data):
@@ -654,13 +756,13 @@ def test_task_status_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -668,6 +770,7 @@ def test_task_status_patch(client, data):
public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk})
private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk})
private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk})
+ blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk})
users = [
None,
@@ -683,6 +786,8 @@ def test_task_status_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_task_status_action_bulk_update_order(client, data):
@@ -717,11 +822,19 @@ def test_task_status_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_task_statuses": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_status_retrieve(client, data):
public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk})
private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk})
private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk})
+ blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk})
users = [
None,
@@ -737,12 +850,15 @@ def test_issue_status_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_issue_status_update(client, data):
public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk})
private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk})
private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk})
+ blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk})
users = [
None,
@@ -770,11 +886,18 @@ def test_issue_status_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, issue_status_data, users)
assert results == [401, 403, 403, 403, 200]
+ issue_status_data = serializers.IssueStatusSerializer(data.blocked_issue_status).data
+ issue_status_data["name"] = "test"
+ issue_status_data = json.dumps(issue_status_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_status_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_status_delete(client, data):
public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk})
private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk})
private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk})
+ blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk})
users = [
None,
@@ -790,6 +913,8 @@ def test_issue_status_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_status_list(client, data):
@@ -815,13 +940,13 @@ def test_issue_status_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -829,6 +954,7 @@ def test_issue_status_patch(client, data):
public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk})
private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk})
private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk})
+ blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk})
users = [
None,
@@ -844,6 +970,8 @@ def test_issue_status_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_status_action_bulk_update_order(client, data):
@@ -878,11 +1006,19 @@ def test_issue_status_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_issue_statuses": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_type_retrieve(client, data):
public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk})
private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk})
private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk})
+ blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk})
users = [
None,
@@ -898,12 +1034,15 @@ def test_issue_type_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_issue_type_update(client, data):
public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk})
private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk})
private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk})
+ blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk})
users = [
None,
@@ -931,11 +1070,18 @@ def test_issue_type_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, issue_type_data, users)
assert results == [401, 403, 403, 403, 200]
+ issue_type_data = serializers.IssueTypeSerializer(data.blocked_issue_type).data
+ issue_type_data["name"] = "test"
+ issue_type_data = json.dumps(issue_type_data)
+ results = helper_test_http_method(client, 'put', blocked_url, issue_type_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_issue_type_delete(client, data):
public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk})
private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk})
private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk})
+ blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk})
users = [
None,
@@ -951,6 +1097,8 @@ def test_issue_type_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_type_list(client, data):
@@ -976,13 +1124,13 @@ def test_issue_type_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -990,6 +1138,7 @@ def test_issue_type_patch(client, data):
public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk})
private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk})
private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk})
+ blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk})
users = [
None,
@@ -1005,6 +1154,8 @@ def test_issue_type_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_issue_type_action_bulk_update_order(client, data):
@@ -1039,11 +1190,19 @@ def test_issue_type_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_issue_types": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_priority_retrieve(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk})
+ blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk})
users = [
None,
@@ -1059,12 +1218,15 @@ def test_priority_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_priority_update(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk})
+ blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk})
users = [
None,
@@ -1092,11 +1254,17 @@ def test_priority_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, priority_data, users)
assert results == [401, 403, 403, 403, 200]
+ priority_data = serializers.PrioritySerializer(data.blocked_priority).data
+ priority_data["name"] = "test"
+ priority_data = json.dumps(priority_data)
+ results = helper_test_http_method(client, 'put', blocked_url, priority_data, users)
+ assert results == [401, 403, 403, 403, 451]
def test_priority_delete(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk})
+ blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk})
users = [
None,
@@ -1112,6 +1280,8 @@ def test_priority_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_priority_list(client, data):
@@ -1137,13 +1307,13 @@ def test_priority_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -1151,6 +1321,7 @@ def test_priority_patch(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk})
+ blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk})
users = [
None,
@@ -1166,6 +1337,8 @@ def test_priority_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_priority_action_bulk_update_order(client, data):
@@ -1200,11 +1373,19 @@ def test_priority_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_priorities": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_severity_retrieve(client, data):
public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk})
private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk})
private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk})
+ blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk})
users = [
None,
@@ -1220,12 +1401,15 @@ def test_severity_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_severity_update(client, data):
public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk})
private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk})
private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk})
+ blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk})
users = [
None,
@@ -1253,11 +1437,18 @@ def test_severity_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, severity_data, users)
assert results == [401, 403, 403, 403, 200]
+ severity_data = serializers.SeveritySerializer(data.blocked_severity).data
+ severity_data["name"] = "test"
+ severity_data = json.dumps(severity_data)
+ results = helper_test_http_method(client, 'put', blocked_url, severity_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_severity_delete(client, data):
public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk})
private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk})
private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk})
+ blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk})
users = [
None,
@@ -1273,6 +1464,8 @@ def test_severity_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_severity_list(client, data):
@@ -1298,13 +1491,13 @@ def test_severity_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
@@ -1312,6 +1505,7 @@ def test_severity_patch(client, data):
public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk})
private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk})
private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk})
+ blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk})
users = [
None,
@@ -1327,6 +1521,8 @@ def test_severity_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_severity_action_bulk_update_order(client, data):
@@ -1361,11 +1557,19 @@ def test_severity_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_severities": [(1, 2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_membership_retrieve(client, data):
public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk})
private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk})
private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk})
+ blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk})
users = [
None,
@@ -1381,12 +1585,15 @@ def test_membership_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_membership_update(client, data):
public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk})
private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk})
private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk})
+ blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk})
users = [
None,
@@ -1414,11 +1621,18 @@ def test_membership_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, membership_data, users)
assert results == [401, 403, 403, 403, 200]
+ membership_data = serializers.MembershipSerializer(data.blocked_membership).data
+ membership_data["token"] = "test"
+ membership_data = json.dumps(membership_data)
+ results = helper_test_http_method(client, 'put', blocked_url, membership_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_membership_delete(client, data):
public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk})
private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk})
private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk})
+ blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk})
users = [
None,
@@ -1434,6 +1648,8 @@ def test_membership_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_membership_list(client, data):
@@ -1459,13 +1675,13 @@ def test_membership_list(client, data):
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 8
+ assert len(projects_data) == 11
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 8
+ assert len(projects_data) == 11
assert response.status_code == 200
@@ -1473,6 +1689,7 @@ def test_membership_patch(client, data):
public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk})
private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk})
private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk})
+ blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk})
users = [
None,
@@ -1488,6 +1705,8 @@ def test_membership_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_membership_create(client, data):
@@ -1522,6 +1741,13 @@ def test_membership_create(client, data):
results = helper_test_http_method(client, 'post', url, membership_data, users)
assert results == [401, 403, 403, 403, 201]
+ membership_data = serializers.MembershipSerializer(data.blocked_membership).data
+ membership_data["id"] = None
+ membership_data["email"] = "test4@test.com"
+ membership_data = json.dumps(membership_data)
+ results = helper_test_http_method(client, 'post', url, membership_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_membership_action_bulk_create(client, data):
url = reverse('memberships-bulk-create')
@@ -1567,15 +1793,27 @@ def test_membership_action_bulk_create(client, data):
results = helper_test_http_method(client, 'post', url, bulk_data, users)
assert results == [401, 403, 403, 403, 200]
+ bulk_data = {
+ "project_id": data.blocked_project.id,
+ "bulk_memberships": [
+ {"role_id": data.private_membership2.role.pk, "email": "test1@test.com"},
+ {"role_id": data.private_membership2.role.pk, "email": "test2@test.com"},
+ ]
+ }
+ bulk_data = json.dumps(bulk_data)
+ results = helper_test_http_method(client, 'post', url, bulk_data, users)
+ assert results == [401, 403, 403, 403, 451]
def test_membership_action_resend_invitation(client, data):
public_invitation = f.InvitationFactory(project=data.public_project, role__project=data.public_project)
private_invitation1 = f.InvitationFactory(project=data.private_project1, role__project=data.private_project1)
private_invitation2 = f.InvitationFactory(project=data.private_project2, role__project=data.private_project2)
+ blocked_invitation = f.InvitationFactory(project=data.blocked_project, role__project=data.blocked_project)
public_url = reverse('memberships-resend-invitation', kwargs={"pk": public_invitation.pk})
private1_url = reverse('memberships-resend-invitation', kwargs={"pk": private_invitation1.pk})
private2_url = reverse('memberships-resend-invitation', kwargs={"pk": private_invitation2.pk})
+ blocked_url = reverse('memberships-resend-invitation', kwargs={"pk": blocked_invitation.pk})
users = [
None,
@@ -1594,6 +1832,9 @@ def test_membership_action_resend_invitation(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 404, 403, 204]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 404, 403, 451]
+
def test_project_template_retrieve(client, data):
url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk})
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 023fee94..f90c10b7 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse
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
@@ -33,6 +34,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
f.RoleFactory(project=m.public_project)
@@ -52,6 +58,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -65,6 +79,10 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
ContentType = apps.get_model("contenttypes", "ContentType")
Project = apps.get_model("projects", "Project")
@@ -76,6 +94,8 @@ def data():
f.LikeFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner)
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
+ f.LikeFactory(content_type=project_ct, object_id=m.blocked_project.pk, user=m.project_member_with_perms)
+ f.LikeFactory(content_type=project_ct, object_id=m.blocked_project.pk, user=m.project_owner)
return m
@@ -84,6 +104,7 @@ def test_project_retrieve(client, data):
public_url = reverse('projects-detail', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-detail', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -98,15 +119,13 @@ def test_project_retrieve(client, data):
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 200, 200]
def test_project_update(client, data):
url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk})
-
- project_data = ProjectDetailSerializer(data.private_project2).data
- project_data["is_private"] = False
-
- project_data = json.dumps(project_data)
+ blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -115,12 +134,20 @@ def test_project_update(client, data):
data.project_owner
]
- results = helper_test_http_method(client, 'put', url, project_data, users)
+ project_data = ProjectDetailSerializer(data.private_project2).data
+ project_data["is_private"] = False
+ results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
assert results == [401, 403, 403, 200]
+ project_data = ProjectDetailSerializer(data.blocked_project).data
+ project_data["is_private"] = False
+ results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
+ assert results == [401, 403, 403, 451]
+
def test_project_delete(client, data):
url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -131,6 +158,9 @@ def test_project_delete(client, data):
results = helper_test_http_method(client, 'delete', url, 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_project_list(client, data):
url = reverse('projects-list')
@@ -151,19 +181,20 @@ def test_project_list(client, data):
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
- assert len(projects_data) == 3
+ assert len(projects_data) == 4
assert response.status_code == 200
def test_project_patch(client, data):
url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -172,14 +203,19 @@ def test_project_patch(client, data):
data.project_owner
]
data = json.dumps({"is_private": False})
+
results = helper_test_http_method(client, 'patch', url, data, users)
assert results == [401, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, data, users)
+ assert results == [401, 403, 403, 451]
+
def test_project_action_stats(client, data):
public_url = reverse('projects-stats', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-stats', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-stats', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-stats', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -193,12 +229,15 @@ def test_project_action_stats(client, data):
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [404, 404, 200, 200]
def test_project_action_issues_stats(client, data):
public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-issues-stats', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -212,12 +251,15 @@ def test_project_action_issues_stats(client, data):
assert results == [200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [404, 404, 200, 200]
def test_project_action_like(client, data):
public_url = reverse('projects-like', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-like', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-like', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-like', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -231,12 +273,15 @@ def test_project_action_like(client, data):
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451, 451]
def test_project_action_unlike(client, data):
public_url = reverse('projects-unlike', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-unlike', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-unlike', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-unlike', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -250,12 +295,15 @@ def test_project_action_unlike(client, data):
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451, 451]
def test_project_fans_list(client, data):
public_url = reverse('project-fans-list', kwargs={"resource_id": data.public_project.pk})
private1_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project1.pk})
private2_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project2.pk})
+ blocked_url = reverse('project-fans-list', kwargs={"resource_id": data.blocked_project.pk})
users = [
None,
@@ -271,6 +319,8 @@ def test_project_fans_list(client, data):
assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)]
results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
+ results = helper_test_http_method_and_count(client, 'get', blocked_url, None, users)
+ assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)]
def test_project_fans_retrieve(client, data):
@@ -280,6 +330,8 @@ def test_project_fans_retrieve(client, data):
"pk": data.project_owner.pk})
private2_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project2.pk,
"pk": data.project_owner.pk})
+ blocked_url = reverse('project-fans-detail', kwargs={"resource_id": data.blocked_project.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -295,12 +347,15 @@ def test_project_fans_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_project_watchers_list(client, data):
public_url = reverse('project-watchers-list', kwargs={"resource_id": data.public_project.pk})
private1_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project1.pk})
private2_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project2.pk})
+ blocked_url = reverse('project-watchers-list', kwargs={"resource_id": data.blocked_project.pk})
users = [
None,
@@ -316,6 +371,8 @@ def test_project_watchers_list(client, data):
assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)]
results = helper_test_http_method_and_count(client, 'get', private2_url, None, users)
assert results == [(401, 0), (403, 0), (403, 0), (200, 3), (200, 3)]
+ results = helper_test_http_method_and_count(client, 'get', blocked_url, None, users)
+ assert results == [(401, 0), (403, 0), (403, 0), (200, 3), (200, 3)]
def test_project_watchers_retrieve(client, data):
@@ -325,6 +382,8 @@ def test_project_watchers_retrieve(client, data):
"pk": data.project_owner.pk})
private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.pk,
"pk": data.project_owner.pk})
+ blocked_url = reverse('project-watchers-detail', kwargs={"resource_id": data.blocked_project.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -340,6 +399,8 @@ def test_project_watchers_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_project_action_create_template(client, data):
@@ -401,6 +462,7 @@ def test_regenerate_userstories_csv_uuid(client, data):
public_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -417,11 +479,15 @@ def test_regenerate_userstories_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 403, 451]
+
def test_regenerate_tasks_csv_uuid(client, data):
public_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -438,11 +504,15 @@ def test_regenerate_tasks_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 403, 451]
+
def test_regenerate_issues_csv_uuid(client, data):
public_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -459,11 +529,15 @@ def test_regenerate_issues_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 403, 451]
+
def test_project_action_watch(client, data):
public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-watch', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-watch', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -477,12 +551,15 @@ def test_project_action_watch(client, data):
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451, 451]
def test_project_action_unwatch(client, data):
public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-unwatch', kwargs={"pk": data.private_project2.pk})
+ blocked_url = reverse('projects-unwatch', kwargs={"pk": data.blocked_project.pk})
users = [
None,
@@ -496,3 +573,5 @@ def test_project_action_unwatch(client, data):
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451, 451]
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 05b5f49d..b02a34a6 100644
--- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
@@ -19,6 +19,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,
ANON_PERMISSIONS, USER_PERMISSIONS)
@@ -52,6 +53,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -81,6 +87,17 @@ def data():
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ email=m.project_member_with_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ email=m.project_member_without_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=[])
+
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
@@ -93,9 +110,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project)
m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1)
m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2)
+ m.blocked_task_ca = f.TaskCustomAttributeFactory(project=m.blocked_project)
m.public_task = f.TaskFactory(project=m.public_project,
status__project=m.public_project,
@@ -109,10 +131,15 @@ def data():
status__project=m.private_project2,
milestone__project=m.private_project2,
user_story__project=m.private_project2)
+ m.blocked_task = f.TaskFactory(project=m.blocked_project,
+ status__project=m.blocked_project,
+ milestone__project=m.blocked_project,
+ user_story__project=m.blocked_project)
m.public_task_cav = m.public_task.custom_attributes_values
m.private_task_cav1 = m.private_task1.custom_attributes_values
m.private_task_cav2 = m.private_task2.custom_attributes_values
+ m.blocked_task_cav = m.blocked_task.custom_attributes_values
return m
@@ -125,6 +152,7 @@ def test_task_custom_attribute_retrieve(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
+ blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk})
users = [
None,
@@ -140,12 +168,15 @@ def test_task_custom_attribute_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_task_custom_attribute_create(client, data):
public_url = reverse('task-custom-attributes-list')
private1_url = reverse('task-custom-attributes-list')
private2_url = reverse('task-custom-attributes-list')
+ blocked_url = reverse('task-custom-attributes-list')
users = [
None,
@@ -170,11 +201,17 @@ def test_task_custom_attribute_create(client, data):
results = helper_test_http_method(client, 'post', private2_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 201]
+ task_ca_data = {"name": "test-new", "project": data.blocked_project.id}
+ task_ca_data = json.dumps(task_ca_data)
+ results = helper_test_http_method(client, 'post', blocked_url, task_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_custom_attribute_update(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
+ blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk})
users = [
None,
@@ -202,11 +239,18 @@ def test_task_custom_attribute_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 200]
+ task_ca_data = serializers.TaskCustomAttributeSerializer(data.blocked_task_ca).data
+ task_ca_data["name"] = "test"
+ task_ca_data = json.dumps(task_ca_data)
+ results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_custom_attribute_delete(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
+ blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk})
users = [
None,
@@ -222,6 +266,9 @@ def test_task_custom_attribute_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_task_custom_attribute_list(client, data):
@@ -243,12 +290,12 @@ def test_task_custom_attribute_list(client, data):
client.login(data.project_member_with_perms)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
@@ -256,6 +303,7 @@ def test_task_custom_attribute_patch(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
+ blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk})
users = [
None,
@@ -271,6 +319,8 @@ def test_task_custom_attribute_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_task_custom_attribute_action_bulk_update_order(client, data):
@@ -305,6 +355,12 @@ def test_task_custom_attribute_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_task_custom_attributes": [(1,2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
#########################################################
# Task Custom Attribute
@@ -315,6 +371,7 @@ def test_task_custom_attributes_values_retrieve(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
+ blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk})
users = [
None,
@@ -330,12 +387,15 @@ def test_task_custom_attributes_values_retrieve(client, data):
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_task_custom_attributes_values_update(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
+ blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk})
users = [
None,
@@ -363,11 +423,18 @@ def test_task_custom_attributes_values_update(client, data):
results = helper_test_http_method(client, 'put', private_url2, task_data, users)
assert results == [401, 403, 403, 200, 200]
+ task_data = serializers.TaskCustomAttributesValuesSerializer(data.blocked_task_cav).data
+ task_data["attributes_values"] = {str(data.blocked_task_ca.pk): "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_custom_attributes_values_patch(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
+ blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk})
users = [
None,
@@ -391,3 +458,8 @@ def test_task_custom_attributes_values_patch(client, data):
"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({"attributes_values": {str(data.blocked_task_ca.pk): "test"},
+ "version": data.blocked_task.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 4a871e8e..88a130c8 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -3,6 +3,7 @@ import uuid
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+from taiga.projects import choices as project_choices
from taiga.projects.tasks.serializers import TaskSerializer
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
@@ -51,6 +52,12 @@ def data():
public_permissions=[],
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ tasks_csv_uuid=uuid.uuid4().hex,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -72,6 +79,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -85,9 +100,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
milestone_public_task = f.MilestoneFactory(project=m.public_project)
milestone_private_task1 = f.MilestoneFactory(project=m.private_project1)
milestone_private_task2 = f.MilestoneFactory(project=m.private_project2)
+ milestone_blocked_task = f.MilestoneFactory(project=m.blocked_project)
m.public_task = f.TaskFactory(project=m.public_project,
status__project=m.public_project,
@@ -104,6 +124,11 @@ def data():
milestone=milestone_private_task2,
user_story__project=m.private_project2,
user_story__milestone=milestone_private_task2)
+ m.blocked_task = f.TaskFactory(project=m.blocked_project,
+ status__project=m.blocked_project,
+ milestone=milestone_blocked_task,
+ user_story__project=m.blocked_project,
+ user_story__milestone=milestone_blocked_task)
m.public_project.default_task_status = m.public_task.status
m.public_project.save()
@@ -111,6 +136,8 @@ def data():
m.private_project1.save()
m.private_project2.default_task_status = m.private_task2.status
m.private_project2.save()
+ m.blocked_project.default_task_status = m.blocked_task.status
+ m.blocked_project.save()
return m
@@ -119,6 +146,7 @@ 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})
private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -134,12 +162,15 @@ def test_task_retrieve(client, data):
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_task_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})
+ blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -168,6 +199,12 @@ def test_task_update(client, 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]
+
def test_task_update_with_project_change(client):
user1 = f.UserFactory.create()
@@ -268,6 +305,7 @@ 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,
@@ -281,6 +319,8 @@ def test_task_delete(client, data):
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):
@@ -302,14 +342,14 @@ def test_task_list(client, data):
response = client.get(url)
tasks_data = json.loads(response.content.decode('utf-8'))
- assert len(tasks_data) == 3
+ 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) == 3
+ assert len(tasks_data) == 4
assert response.status_code == 200
@@ -351,11 +391,21 @@ def test_task_create(client, data):
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):
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,
@@ -378,6 +428,10 @@ def test_task_patch(client, data):
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_task.version})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_task_action_bulk_create(client, data):
url = reverse('tasks-bulk-create')
@@ -417,11 +471,21 @@ def test_task_action_bulk_create(client, data):
results = helper_test_http_method(client, 'post', url, bulk_data, users)
assert results == [401, 403, 403, 200, 200]
+ bulk_data = json.dumps({
+ "bulk_tasks": "test1\ntest2",
+ "us_id": data.blocked_task.user_story.pk,
+ "project_id": data.blocked_task.project.pk,
+ "sprint_id": data.blocked_task.milestone.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, bulk_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_task_action_upvote(client, data):
public_url = reverse('tasks-upvote', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-upvote', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-upvote', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-upvote', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -437,12 +501,15 @@ def test_task_action_upvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_task_action_downvote(client, data):
public_url = reverse('tasks-downvote', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-downvote', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-downvote', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-downvote', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -458,12 +525,15 @@ def test_task_action_downvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_task_voters_list(client, data):
public_url = reverse('task-voters-list', kwargs={"resource_id": data.public_task.pk})
private_url1 = reverse('task-voters-list', kwargs={"resource_id": data.private_task1.pk})
private_url2 = reverse('task-voters-list', kwargs={"resource_id": data.private_task2.pk})
+ blocked_url = reverse('task-voters-list', kwargs={"resource_id": data.blocked_task.pk})
users = [
None,
@@ -479,6 +549,8 @@ def test_task_voters_list(client, data):
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_task_voters_retrieve(client, data):
@@ -492,6 +564,10 @@ def test_task_voters_retrieve(client, data):
private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk})
+ add_vote(data.blocked_task, data.project_owner)
+ blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk,
+ "pk": data.project_owner.pk})
+
users = [
None,
data.registered_user,
@@ -506,6 +582,8 @@ def test_task_voters_retrieve(client, data):
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_tasks_csv(client, data):
@@ -513,6 +591,7 @@ def test_tasks_csv(client, data):
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,
@@ -531,11 +610,15 @@ def test_tasks_csv(client, data):
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})
private_url2 = reverse('tasks-watch', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-watch', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -551,12 +634,15 @@ def test_task_action_watch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_task_action_unwatch(client, data):
public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-unwatch', kwargs={"pk": data.private_task2.pk})
+ blocked_url = reverse('tasks-unwatch', kwargs={"pk": data.blocked_task.pk})
users = [
None,
@@ -572,12 +658,15 @@ def test_task_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_task_watchers_list(client, data):
public_url = reverse('task-watchers-list', kwargs={"resource_id": data.public_task.pk})
private_url1 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task1.pk})
private_url2 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task2.pk})
+ blocked_url = reverse('task-watchers-list', kwargs={"resource_id": data.blocked_task.pk})
users = [
None,
@@ -593,6 +682,8 @@ def test_task_watchers_list(client, data):
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_task_watchers_retrieve(client, data):
@@ -606,6 +697,9 @@ def test_task_watchers_retrieve(client, data):
private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk})
+ add_watcher(data.blocked_task, data.project_owner)
+ blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk,
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
@@ -620,3 +714,5 @@ def test_task_watchers_retrieve(client, data):
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]
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 a665e566..7c951322 100644
--- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
@@ -19,6 +19,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,
ANON_PERMISSIONS, USER_PERMISSIONS)
@@ -53,18 +54,23 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.public_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
-
f.MembershipFactory(project=m.private_project1,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
@@ -82,6 +88,17 @@ def data():
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ email=m.project_member_with_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ email=m.project_member_without_perms.email,
+ role__project=m.blocked_project,
+ role__permissions=[])
+
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
@@ -94,10 +111,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project)
m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1)
m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2)
-
+ m.blocked_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.blocked_project)
m.public_user_story = f.UserStoryFactory(project=m.public_project,
status__project=m.public_project)
@@ -105,10 +126,13 @@ def data():
status__project=m.private_project1)
m.private_user_story2 = f.UserStoryFactory(project=m.private_project2,
status__project=m.private_project2)
+ m.blocked_user_story = f.UserStoryFactory(project=m.blocked_project,
+ status__project=m.blocked_project)
m.public_user_story_cav = m.public_user_story.custom_attributes_values
m.private_user_story_cav1 = m.private_user_story1.custom_attributes_values
m.private_user_story_cav2 = m.private_user_story2.custom_attributes_values
+ m.blocked_user_story_cav = m.blocked_user_story.custom_attributes_values
return m
@@ -121,6 +145,7 @@ def test_userstory_custom_attribute_retrieve(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
+ blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk})
users = [
None,
@@ -136,12 +161,15 @@ def test_userstory_custom_attribute_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_userstory_custom_attribute_create(client, data):
public_url = reverse('userstory-custom-attributes-list')
private1_url = reverse('userstory-custom-attributes-list')
private2_url = reverse('userstory-custom-attributes-list')
+ blocked_url = reverse('userstory-custom-attributes-list')
users = [
None,
@@ -166,11 +194,17 @@ def test_userstory_custom_attribute_create(client, data):
results = helper_test_http_method(client, 'post', private2_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 201]
+ userstory_ca_data = {"name": "test-new", "project": data.blocked_project.id}
+ userstory_ca_data = json.dumps(userstory_ca_data)
+ results = helper_test_http_method(client, 'post', blocked_url, userstory_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_userstory_custom_attribute_update(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
+ blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk})
users = [
None,
@@ -198,11 +232,18 @@ def test_userstory_custom_attribute_update(client, data):
results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 200]
+ userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.blocked_userstory_ca).data
+ userstory_ca_data["name"] = "test"
+ userstory_ca_data = json.dumps(userstory_ca_data)
+ results = helper_test_http_method(client, 'put', blocked_url, userstory_ca_data, users)
+ assert results == [401, 403, 403, 403, 451]
+
def test_userstory_custom_attribute_delete(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
+ blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk})
users = [
None,
@@ -218,6 +259,8 @@ def test_userstory_custom_attribute_delete(client, data):
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 403, 403, 451]
def test_userstory_custom_attribute_list(client, data):
@@ -239,12 +282,12 @@ def test_userstory_custom_attribute_list(client, data):
client.login(data.project_member_with_perms)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
- assert len(response.data) == 3
+ assert len(response.data) == 4
assert response.status_code == 200
@@ -252,6 +295,7 @@ def test_userstory_custom_attribute_patch(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
+ blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk})
users = [
None,
@@ -267,6 +311,8 @@ def test_userstory_custom_attribute_patch(client, data):
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
+ results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
+ assert results == [401, 403, 403, 403, 451]
def test_userstory_custom_attribute_action_bulk_update_order(client, data):
@@ -301,6 +347,12 @@ def test_userstory_custom_attribute_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
+ post_data = json.dumps({
+ "bulk_userstory_custom_attributes": [(1,2)],
+ "project": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 451]
#########################################################
@@ -315,6 +367,8 @@ def test_userstory_custom_attributes_values_retrieve(client, data):
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
+ blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={
+ "user_story_id": data.blocked_user_story.pk})
users = [
None,
@@ -330,6 +384,8 @@ def test_userstory_custom_attributes_values_retrieve(client, data):
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_userstory_custom_attributes_values_update(client, data):
@@ -339,7 +395,8 @@ def test_userstory_custom_attributes_values_update(client, data):
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
-
+ blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={
+ "user_story_id": data.blocked_user_story.pk})
users = [
None,
data.registered_user,
@@ -366,6 +423,12 @@ def test_userstory_custom_attributes_values_update(client, data):
results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
assert results == [401, 403, 403, 200, 200]
+ user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.blocked_user_story_cav).data
+ user_story_data["attributes_values"] = {str(data.blocked_userstory_ca.pk): "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_userstory_custom_attributes_values_patch(client, data):
public_url = reverse('userstory-custom-attributes-values-detail', kwargs={
@@ -374,7 +437,8 @@ def test_userstory_custom_attributes_values_patch(client, data):
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
-
+ blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={
+ "user_story_id": data.blocked_user_story.pk})
users = [
None,
data.registered_user,
@@ -397,3 +461,8 @@ def test_userstory_custom_attributes_values_patch(client, data):
"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({"attributes_values": {str(data.blocked_userstory_ca.pk): "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]
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 20881aed..3d7a3dd7 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -3,6 +3,7 @@ import uuid
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+from taiga.projects import choices as project_choices
from taiga.projects.userstories.serializers import UserStorySerializer
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
@@ -51,6 +52,12 @@ def data():
public_permissions=[],
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ userstories_csv_uuid=uuid.uuid4().hex,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -72,6 +79,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -85,9 +100,14 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
m.private_points2 = f.PointsFactory(project=m.private_project2)
+ m.blocked_points = f.PointsFactory(project=m.blocked_project)
m.public_role_points = f.RolePointsFactory(role=m.public_project.roles.all()[0],
points=m.public_points,
@@ -104,10 +124,16 @@ def data():
user_story__project=m.private_project2,
user_story__milestone__project=m.private_project2,
user_story__status__project=m.private_project2)
+ m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0],
+ points=m.blocked_points,
+ user_story__project=m.blocked_project,
+ user_story__milestone__project=m.blocked_project,
+ user_story__status__project=m.blocked_project)
m.public_user_story = m.public_role_points.user_story
m.private_user_story1 = m.private_role_points1.user_story
m.private_user_story2 = m.private_role_points2.user_story
+ m.blocked_user_story = m.blocked_role_points.user_story
return m
@@ -116,6 +142,7 @@ 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})
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,
@@ -131,12 +158,15 @@ def test_user_story_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_user_story_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})
+ blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk})
users = [
None,
@@ -165,6 +195,11 @@ def test_user_story_update(client, 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]
def test_user_story_update_with_project_change(client):
user1 = f.UserFactory.create()
@@ -265,6 +300,7 @@ 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,
@@ -278,6 +314,8 @@ def test_user_story_delete(client, data):
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):
@@ -299,14 +337,14 @@ def test_user_story_list(client, data):
response = client.get(url)
userstories_data = json.loads(response.content.decode('utf-8'))
- assert len(userstories_data) == 3
+ 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) == 3
+ assert len(userstories_data) == 4
assert response.status_code == 200
@@ -333,11 +371,16 @@ def test_user_story_create(client, data):
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):
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,
@@ -360,6 +403,10 @@ def test_user_story_patch(client, data):
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]
+
def test_user_story_action_bulk_create(client, data):
url = reverse('userstories-bulk-create')
@@ -384,6 +431,10 @@ def test_user_story_action_bulk_create(client, data):
results = helper_test_http_method(client, 'post', url, bulk_data, users)
assert results == [401, 403, 403, 200, 200]
+ bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.blocked_user_story.project.pk})
+ results = helper_test_http_method(client, 'post', url, bulk_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
def test_user_story_action_bulk_update_order(client, data):
url = reverse('userstories-bulk-update-backlog-order')
@@ -417,10 +468,19 @@ def test_user_story_action_bulk_update_order(client, data):
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 204, 204]
+ post_data = json.dumps({
+ "bulk_stories": [{"us_id": data.blocked_user_story.id, "order": 2}],
+ "project_id": data.blocked_project.pk
+ })
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 451, 451]
+
+
def test_user_story_action_upvote(client, data):
public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-upvote', kwargs={"pk": data.blocked_user_story.pk})
users = [
None,
@@ -436,12 +496,15 @@ def test_user_story_action_upvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_user_story_action_downvote(client, data):
public_url = reverse('userstories-downvote', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-downvote', kwargs={"pk": data.blocked_user_story.pk})
users = [
None,
@@ -457,12 +520,15 @@ def test_user_story_action_downvote(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_user_story_voters_list(client, data):
public_url = reverse('userstory-voters-list', kwargs={"resource_id": data.public_user_story.pk})
private_url1 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story2.pk})
+ blocked_url = reverse('userstory-voters-list', kwargs={"resource_id": data.blocked_user_story.pk})
users = [
None,
@@ -478,6 +544,8 @@ def test_user_story_voters_list(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_user_story_voters_retrieve(client, data):
@@ -491,6 +559,9 @@ def test_user_story_voters_retrieve(client, data):
private_url2 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story2.pk,
"pk": data.project_owner.pk})
+ add_vote(data.blocked_user_story, data.project_owner)
+ blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk,
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
@@ -505,6 +576,8 @@ def test_user_story_voters_retrieve(client, data):
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 403, 200, 200]
def test_user_stories_csv(client, data):
@@ -535,6 +608,7 @@ 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})
private_url2 = reverse('userstories-watch', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-watch', kwargs={"pk": data.blocked_user_story.pk})
users = [
None,
@@ -550,12 +624,15 @@ def test_user_story_action_watch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_user_story_action_unwatch(client, data):
public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story2.pk})
+ blocked_url = reverse('userstories-unwatch', kwargs={"pk": data.blocked_user_story.pk})
users = [
None,
@@ -571,12 +648,15 @@ def test_user_story_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_userstory_watchers_list(client, data):
public_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.public_user_story.pk})
private_url1 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story2.pk})
+ blocked_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.blocked_user_story.pk})
users = [
None,
@@ -592,6 +672,8 @@ def test_userstory_watchers_list(client, data):
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_userstory_watchers_retrieve(client, data):
@@ -604,6 +686,9 @@ def test_userstory_watchers_retrieve(client, data):
add_watcher(data.private_user_story2, data.project_owner)
private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk,
"pk": data.project_owner.pk})
+ add_watcher(data.blocked_user_story, data.project_owner)
+ blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk,
+ "pk": data.project_owner.pk})
users = [
None,
@@ -619,3 +704,5 @@ def test_userstory_watchers_retrieve(client, data):
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]
diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py
index 63ef52e6..30c72097 100644
--- a/tests/integration/resources_permissions/test_webhooks_resources.py
+++ b/tests/integration/resources_permissions/test_webhooks_resources.py
@@ -1,6 +1,7 @@
from django.core.urlresolvers import reverse
from taiga.base.utils import json
+from taiga.projects import choices as project_choices
from taiga.webhooks.serializers import WebhookSerializer
from taiga.webhooks.models import Webhook
@@ -36,15 +37,25 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
f.MembershipFactory(project=m.project1,
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
m.webhook1 = f.WebhookFactory(project=m.project1)
m.webhooklog1 = f.WebhookLogFactory(webhook=m.webhook1)
m.webhook2 = f.WebhookFactory(project=m.project2)
m.webhooklog2 = f.WebhookLogFactory(webhook=m.webhook2)
+ m.blocked_webhook = f.WebhookFactory(project=m.blocked_project)
+ m.blocked_webhooklog = f.WebhookLogFactory(webhook=m.blocked_webhook)
return m
@@ -52,6 +63,7 @@ def data():
def test_webhook_retrieve(client, data):
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
+ blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -63,11 +75,14 @@ def test_webhook_retrieve(client, data):
assert results == [401, 403, 200]
results = helper_test_http_method(client, 'get', url2, None, users)
assert results == [401, 403, 403]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 200]
def test_webhook_update(client, data):
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
+ blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -87,10 +102,17 @@ def test_webhook_update(client, data):
results = helper_test_http_method(client, 'put', url2, webhook_data, users)
assert results == [401, 403, 403]
+ webhook_data = WebhookSerializer(data.blocked_webhook).data
+ webhook_data["key"] = "test"
+ webhook_data = json.dumps(webhook_data)
+ results = helper_test_http_method(client, 'put', blocked_url, webhook_data, users)
+ assert results == [401, 403, 451]
+
def test_webhook_delete(client, data):
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
+ blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -101,6 +123,8 @@ def test_webhook_delete(client, data):
assert results == [401, 403, 204]
results = helper_test_http_method(client, 'delete', url2, None, users)
assert results == [401, 403, 403]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [401, 403, 451]
def test_webhook_list(client, data):
@@ -122,7 +146,7 @@ def test_webhook_list(client, data):
response = client.get(url)
webhooks_data = json.loads(response.content.decode('utf-8'))
- assert len(webhooks_data) == 1
+ assert len(webhooks_data) == 2
assert response.status_code == 200
@@ -153,10 +177,20 @@ def test_webhook_create(client, data):
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete())
assert results == [401, 403, 403]
+ create_data = json.dumps({
+ "name": "Test",
+ "url": "http://test.com",
+ "key": "test",
+ "project": data.blocked_project.pk,
+ })
+ results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete())
+ assert results == [401, 403, 451]
+
def test_webhook_patch(client, data):
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
+ blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -172,10 +206,15 @@ def test_webhook_patch(client, data):
results = helper_test_http_method(client, 'patch', url2, patch_data, users)
assert results == [401, 403, 403]
+ patch_data = json.dumps({"key": "test"})
+ results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users)
+ assert results == [401, 403, 451]
+
def test_webhook_action_test(client, data):
url1 = reverse('webhooks-test', kwargs={"pk": data.webhook1.pk})
url2 = reverse('webhooks-test', kwargs={"pk": data.webhook2.pk})
+ blocked_url = reverse('webhooks-test', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -193,6 +232,11 @@ def test_webhook_action_test(client, data):
assert results == [404, 404, 404]
assert _send_request_mock.called is False
+ with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451]
+ assert _send_request_mock.called is False
+
def test_webhooklogs_list(client, data):
url = reverse('webhooklogs-list')
@@ -213,13 +257,14 @@ def test_webhooklogs_list(client, data):
response = client.get(url)
webhooklogs_data = json.loads(response.content.decode('utf-8'))
- assert len(webhooklogs_data) == 1
+ assert len(webhooklogs_data) == 2
assert response.status_code == 200
def test_webhooklogs_retrieve(client, data):
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
+ blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk})
users = [
None,
@@ -233,10 +278,14 @@ def test_webhooklogs_retrieve(client, data):
results = helper_test_http_method(client, 'get', url2, None, users)
assert results == [401, 403, 403]
+ results = helper_test_http_method(client, 'get', blocked_url, None, users)
+ assert results == [401, 403, 200]
+
def test_webhooklogs_create(client, data):
url1 = reverse('webhooklogs-list')
url2 = reverse('webhooklogs-list')
+ blocked_url = reverse('webhooklogs-list')
users = [
None,
@@ -250,10 +299,14 @@ def test_webhooklogs_create(client, data):
results = helper_test_http_method(client, 'post', url2, None, users)
assert results == [405, 405, 405]
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [405, 405, 405]
+
def test_webhooklogs_delete(client, data):
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
+ blocked_url = reverse('webhooklogs-detail', kwargs={"pk": data.blocked_webhooklog.pk})
users = [
None,
@@ -267,10 +320,14 @@ def test_webhooklogs_delete(client, data):
results = helper_test_http_method(client, 'delete', url2, None, users)
assert results == [405, 405, 405]
+ results = helper_test_http_method(client, 'delete', blocked_url, None, users)
+ assert results == [405, 405, 405]
+
def test_webhooklogs_update(client, data):
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
+ blocked_url = reverse('webhooklogs-detail', kwargs={"pk": data.blocked_webhooklog.pk})
users = [
None,
@@ -284,16 +341,23 @@ def test_webhooklogs_update(client, data):
results = helper_test_http_method(client, 'put', url2, None, users)
assert results == [405, 405, 405]
+ results = helper_test_http_method(client, 'put', blocked_url, None, users)
+ assert results == [405, 405, 405]
+
results = helper_test_http_method(client, 'patch', url1, None, users)
assert results == [405, 405, 405]
results = helper_test_http_method(client, 'patch', url2, None, users)
assert results == [405, 405, 405]
+ results = helper_test_http_method(client, 'patch', blocked_url, None, users)
+ assert results == [405, 405, 405]
+
def test_webhooklogs_action_resend(client, data):
url1 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog1.pk})
url2 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog2.pk})
+ blocked_url = reverse('webhooklogs-resend', kwargs={"pk": data.blocked_webhooklog.pk})
users = [
None,
@@ -306,3 +370,6 @@ def test_webhooklogs_action_resend(client, data):
results = helper_test_http_method(client, 'post', url2, None, users)
assert results == [404, 404, 404]
+
+ results = helper_test_http_method(client, 'post', blocked_url, None, users)
+ assert results == [404, 404, 451]
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index 14f2f92b..ecbfb83b 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -2,6 +2,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.projects import choices as project_choices
from taiga.projects.notifications.services import add_watcher
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer
@@ -46,6 +47,11 @@ def data():
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -67,6 +73,14 @@ def data():
user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_member_without_perms,
+ role__project=m.blocked_project,
+ role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
@@ -80,13 +94,19 @@ def data():
user=m.project_owner,
is_owner=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_owner=True)
+
m.public_wiki_page = f.WikiPageFactory(project=m.public_project)
m.private_wiki_page1 = f.WikiPageFactory(project=m.private_project1)
m.private_wiki_page2 = f.WikiPageFactory(project=m.private_project2)
+ m.blocked_wiki_page = f.WikiPageFactory(project=m.blocked_project)
m.public_wiki_link = f.WikiLinkFactory(project=m.public_project)
m.private_wiki_link1 = f.WikiLinkFactory(project=m.private_project1)
m.private_wiki_link2 = f.WikiLinkFactory(project=m.private_project2)
+ m.blocked_wiki_link = f.WikiLinkFactory(project=m.blocked_project)
return m
@@ -95,6 +115,7 @@ 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,
@@ -110,12 +131,15 @@ def test_wiki_page_retrieve(client, data):
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,
@@ -144,11 +168,18 @@ def test_wiki_page_update(client, 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,
@@ -162,6 +193,8 @@ def test_wiki_page_delete(client, data):
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_wiki_page_list(client, data):
@@ -183,14 +216,14 @@ def test_wiki_page_list(client, data):
response = client.get(url)
wiki_pages_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_pages_data) == 3
+ assert len(wiki_pages_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
wiki_pages_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_pages_data) == 3
+ assert len(wiki_pages_data) == 4
assert response.status_code == 200
@@ -229,11 +262,19 @@ 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, 201, 201]
+ create_data = json.dumps({
+ "content": "test",
+ "slug": "test",
+ "project": data.blocked_project.pk,
+ })
+ 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):
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,
@@ -256,6 +297,10 @@ def test_wiki_page_patch(client, data):
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_action_render(client, data):
url = reverse('wiki-render')
@@ -277,6 +322,7 @@ 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,
@@ -292,12 +338,15 @@ def test_wiki_link_retrieve(client, data):
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})
users = [
None,
@@ -326,11 +375,17 @@ def test_wiki_link_update(client, 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_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,
@@ -344,6 +399,8 @@ def test_wiki_link_delete(client, data):
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_wiki_link_list(client, data):
@@ -365,14 +422,14 @@ def test_wiki_link_list(client, data):
response = client.get(url)
wiki_links_data = json.loads(response.content.decode('utf-8'))
- assert len(wiki_links_data) == 3
+ 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) == 3
+ assert len(wiki_links_data) == 4
assert response.status_code == 200
@@ -411,11 +468,20 @@ def test_wiki_link_create(client, data):
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,
@@ -438,11 +504,16 @@ def test_wiki_link_patch(client, data):
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_wikipage_action_watch(client, data):
public_url = reverse('wiki-watch', kwargs={"pk": data.public_wiki_page.pk})
private_url1 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-watch', kwargs={"pk": data.blocked_wiki_page.pk})
users = [
None,
@@ -458,12 +529,15 @@ def test_wikipage_action_watch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_wikipage_action_unwatch(client, data):
public_url = reverse('wiki-unwatch', kwargs={"pk": data.public_wiki_page.pk})
private_url1 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-unwatch', kwargs={"pk": data.blocked_wiki_page.pk})
users = [
None,
@@ -479,12 +553,15 @@ def test_wikipage_action_unwatch(client, data):
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
+ results = helper_test_http_method(client, 'post', blocked_url, "", users)
+ assert results == [404, 404, 404, 451, 451]
def test_wikipage_watchers_list(client, data):
public_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.public_wiki_page.pk})
private_url1 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page1.pk})
private_url2 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page2.pk})
+ blocked_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.blocked_wiki_page.pk})
users = [
None,
@@ -500,6 +577,8 @@ def test_wikipage_watchers_list(client, data):
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_wikipage_watchers_retrieve(client, data):
@@ -512,7 +591,9 @@ def test_wikipage_watchers_retrieve(client, data):
add_watcher(data.private_wiki_page2, data.project_owner)
private_url2 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page2.pk,
"pk": data.project_owner.pk})
-
+ add_watcher(data.blocked_wiki_page, data.project_owner)
+ blocked_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.blocked_wiki_page.pk,
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
@@ -527,3 +608,5 @@ def test_wikipage_watchers_retrieve(client, data):
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]
diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py
index f387474d..c400f3ad 100644
--- a/tests/integration/test_vote_tasks.py
+++ b/tests/integration/test_vote_tasks.py
@@ -26,7 +26,7 @@ pytestmark = pytest.mark.django_db
def test_upvote_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-upvote", args=(task.id,))
@@ -38,7 +38,7 @@ def test_upvote_task(client):
def test_downvote_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-downvote", args=(task.id,))
@@ -93,7 +93,7 @@ def test_get_task_votes(client):
def test_get_task_is_voted(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
f.VotesFactory.create(content_object=task)
url_detail = reverse("tasks-detail", args=(task.id,))
diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py
index 772937b8..4fbc0e6b 100644
--- a/tests/integration/test_vote_userstories.py
+++ b/tests/integration/test_vote_userstories.py
@@ -26,7 +26,7 @@ pytestmark = pytest.mark.django_db
def test_upvote_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-upvote", args=(user_story.id,))
@@ -38,7 +38,7 @@ def test_upvote_user_story(client):
def test_downvote_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-downvote", args=(user_story.id,))
@@ -92,7 +92,7 @@ def test_get_user_story_votes(client):
def test_get_user_story_is_voted(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
f.VotesFactory.create(content_object=user_story)
url_detail = reverse("userstories-detail", args=(user_story.id,))
diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py
index c5010827..885e0a96 100644
--- a/tests/integration/test_watch_issues.py
+++ b/tests/integration/test_watch_issues.py
@@ -79,7 +79,7 @@ def test_get_issue_watcher(client):
def test_get_issue_watchers(client):
user = f.UserFactory.create()
- issue = f.IssueFactory(owner=user)
+ issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
url = reverse("issues-detail", args=(issue.id,))
@@ -95,7 +95,7 @@ def test_get_issue_watchers(client):
def test_get_issue_is_watcher(client):
user = f.UserFactory.create()
- issue = f.IssueFactory(owner=user)
+ issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
url_detail = reverse("issues-detail", args=(issue.id,))
url_watch = reverse("issues-watch", args=(issue.id,))
diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py
index cde26ed0..c0fbc59b 100644
--- a/tests/integration/test_watch_tasks.py
+++ b/tests/integration/test_watch_tasks.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_watch_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-watch", args=(task.id,))
@@ -39,7 +39,7 @@ def test_watch_task(client):
def test_unwatch_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-watch", args=(task.id,))
@@ -95,7 +95,7 @@ def test_get_task_watchers(client):
def test_get_task_is_watcher(client):
user = f.UserFactory.create()
- task = f.TaskFactory(owner=user)
+ task = f.create_task(owner=user, milestone=None)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url_detail = reverse("tasks-detail", args=(task.id,))
url_watch = reverse("tasks-watch", args=(task.id,))
@@ -109,6 +109,7 @@ def test_get_task_is_watcher(client):
assert response.data['is_watcher'] == False
response = client.post(url_watch)
+ print(response.data)
assert response.status_code == 200
response = client.get(url_detail)
diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py
index e17081cd..7e940664 100644
--- a/tests/integration/test_watch_userstories.py
+++ b/tests/integration/test_watch_userstories.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_watch_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-watch", args=(user_story.id,))
@@ -39,7 +39,7 @@ def test_watch_user_story(client):
def test_unwatch_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-unwatch", args=(user_story.id,))
@@ -65,7 +65,7 @@ def test_list_user_story_watchers(client):
def test_get_user_story_watcher(client):
user = f.UserFactory.create()
- user_story = f.UserStoryFactory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
watch = f.WatchedFactory.create(content_object=user_story, user=user)
url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id))
@@ -79,7 +79,7 @@ def test_get_user_story_watcher(client):
def test_get_user_story_watchers(client):
user = f.UserFactory.create()
- user_story = f.UserStoryFactory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-detail", args=(user_story.id,))
@@ -95,7 +95,7 @@ def test_get_user_story_watchers(client):
def test_get_user_story_is_watcher(client):
user = f.UserFactory.create()
- user_story = f.UserStoryFactory(owner=user)
+ user_story = f.create_userstory(owner=user, status=None)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url_detail = reverse("userstories-detail", args=(user_story.id,))
url_watch = reverse("userstories-watch", args=(user_story.id,))
From 53ad5fb9fc00d2b17cc3e28d0e4ae853ca61c2e8 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 27 Jan 2016 12:29:55 +0100
Subject: [PATCH 007/105] Excluding blocked projects from sitemap
---
taiga/front/sitemaps/issues.py | 3 +++
taiga/front/sitemaps/milestones.py | 3 +++
taiga/front/sitemaps/projects.py | 3 +++
taiga/front/sitemaps/tasks.py | 3 +++
taiga/front/sitemaps/userstories.py | 3 +++
taiga/front/sitemaps/wiki.py | 3 +++
6 files changed, 18 insertions(+)
diff --git a/taiga/front/sitemaps/issues.py b/taiga/front/sitemaps/issues.py
index 38d70d70..bf5694e0 100644
--- a/taiga/front/sitemaps/issues.py
+++ b/taiga/front/sitemaps/issues.py
@@ -32,6 +32,9 @@ class IssuesSitemap(Sitemap):
Q(project__is_private=True,
project__anon_permissions__contains=["view_issues"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
# Project data is needed
queryset = queryset.select_related("project")
diff --git a/taiga/front/sitemaps/milestones.py b/taiga/front/sitemaps/milestones.py
index 73a1c064..274cbaf8 100644
--- a/taiga/front/sitemaps/milestones.py
+++ b/taiga/front/sitemaps/milestones.py
@@ -34,6 +34,9 @@ class MilestonesSitemap(Sitemap):
"view_us",
"view_tasks"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
# Project data is needed
queryset = queryset.select_related("project")
diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py
index 80cd483b..cdfc8cc2 100644
--- a/taiga/front/sitemaps/projects.py
+++ b/taiga/front/sitemaps/projects.py
@@ -32,6 +32,9 @@ class ProjectsSitemap(Sitemap):
Q(is_private=True,
anon_permissions__contains=["view_project"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(blocked_code__isnull=True)
+
return queryset
def location(self, obj):
diff --git a/taiga/front/sitemaps/tasks.py b/taiga/front/sitemaps/tasks.py
index a1b2570e..eaa599ff 100644
--- a/taiga/front/sitemaps/tasks.py
+++ b/taiga/front/sitemaps/tasks.py
@@ -32,6 +32,9 @@ class TasksSitemap(Sitemap):
Q(project__is_private=True,
project__anon_permissions__contains=["view_tasks"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
# Project data is needed
queryset = queryset.select_related("project")
diff --git a/taiga/front/sitemaps/userstories.py b/taiga/front/sitemaps/userstories.py
index 0d59c323..9d66773c 100644
--- a/taiga/front/sitemaps/userstories.py
+++ b/taiga/front/sitemaps/userstories.py
@@ -32,6 +32,9 @@ class UserStoriesSitemap(Sitemap):
Q(project__is_private=True,
project__anon_permissions__contains=["view_us"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
# Project data is needed
queryset = queryset.select_related("project")
diff --git a/taiga/front/sitemaps/wiki.py b/taiga/front/sitemaps/wiki.py
index 7021d393..85e03ba0 100644
--- a/taiga/front/sitemaps/wiki.py
+++ b/taiga/front/sitemaps/wiki.py
@@ -32,6 +32,9 @@ class WikiPagesSitemap(Sitemap):
Q(project__is_private=True,
project__anon_permissions__contains=["view_wiki_pages"]))
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
# Exclude wiki pages from projects without wiki section enabled
queryset = queryset.exclude(project__is_wiki_activated=False)
From a263e79e390c9176010adaf5278052caee1e6ede Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 27 Jan 2016 12:48:38 +0100
Subject: [PATCH 008/105] Controlling blocked projects for github, gitlab and
bitbucket hooks
---
taiga/hooks/api.py | 3 +++
tests/integration/test_hooks_bitbucket.py | 21 +++++++++++++++++++++
tests/integration/test_hooks_github.py | 19 +++++++++++++++++++
tests/integration/test_hooks_gitlab.py | 21 +++++++++++++++++++++
4 files changed, 64 insertions(+)
diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py
index e2aea495..6590832e 100644
--- a/taiga/hooks/api.py
+++ b/taiga/hooks/api.py
@@ -64,6 +64,9 @@ class BaseWebhookApiViewSet(GenericViewSet):
if not self._validate_signature(project, request):
raise exc.BadRequest(_("Bad signature"))
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
event_name = self._get_event_name(request)
payload = self._get_payload(request)
diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py
index 1dbf8ea2..f31a674d 100644
--- a/tests/integration/test_hooks_bitbucket.py
+++ b/tests/integration/test_hooks_bitbucket.py
@@ -11,6 +11,7 @@ from taiga.base.utils import json
from taiga.hooks.bitbucket import event_hooks
from taiga.hooks.bitbucket.api import BitBucketViewSet
from taiga.hooks.exceptions import ActionSyntaxException
+from taiga.projects import choices as project_choices
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.userstories.models import UserStory
@@ -80,6 +81,26 @@ def test_ok_signature_ip_in_network(client):
assert response.status_code == 204
+def test_blocked_project(client):
+ project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF)
+ f.ProjectModulesConfigFactory(project=project, config={
+ "bitbucket": {
+ "secret": "tpnIwJDz4e"
+ }
+ })
+
+ url = reverse("bitbucket-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = json.dumps({"push": {"changes": [{"new": {"target": { "message": "test message"}}}]}})
+ response = client.post(url,
+ data,
+ content_type="application/json",
+ HTTP_X_EVENT_KEY="repo:push",
+ REMOTE_ADDR=settings.BITBUCKET_VALID_ORIGIN_IPS[0])
+
+ assert response.status_code == 451
+
+
def test_invalid_ip(client):
project = f.ProjectFactory()
f.ProjectModulesConfigFactory(project=project, config={
diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py
index 5b832643..06133ac2 100644
--- a/tests/integration/test_hooks_github.py
+++ b/tests/integration/test_hooks_github.py
@@ -9,6 +9,7 @@ from taiga.base.utils import json
from taiga.hooks.github import event_hooks
from taiga.hooks.github.api import GitHubViewSet
from taiga.hooks.exceptions import ActionSyntaxException
+from taiga.projects import choices as project_choices
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.userstories.models import UserStory
@@ -53,6 +54,24 @@ def test_ok_signature(client):
assert response.status_code == 204
+def test_blocked_project(client):
+ project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF)
+ f.ProjectModulesConfigFactory(project=project, config={
+ "github": {
+ "secret": "tpnIwJDz4e"
+ }
+ })
+
+ url = reverse("github-hook-list")
+ url = "%s?project=%s" % (url, project.id)
+ data = {"test:": "data"}
+ response = client.post(url, json.dumps(data),
+ HTTP_X_HUB_SIGNATURE="sha1=3c8e83fdaa266f81c036ea0b71e98eb5e054581a",
+ content_type="application/json")
+
+ assert response.status_code == 451
+
+
def test_push_event_detected(client):
project = f.ProjectFactory()
url = reverse("github-hook-list")
diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py
index cc2f8e66..de53bff6 100644
--- a/tests/integration/test_hooks_gitlab.py
+++ b/tests/integration/test_hooks_gitlab.py
@@ -9,6 +9,7 @@ from taiga.base.utils import json
from taiga.hooks.gitlab import event_hooks
from taiga.hooks.gitlab.api import GitLabViewSet
from taiga.hooks.exceptions import ActionSyntaxException
+from taiga.projects import choices as project_choices
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.userstories.models import UserStory
@@ -79,6 +80,26 @@ def test_ok_signature_ip_in_network(client):
assert response.status_code == 204
+def test_blocked_project(client):
+ project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF)
+ f.ProjectModulesConfigFactory(project=project, config={
+ "gitlab": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["111.111.111.111"],
+ }
+ })
+
+ url = reverse("gitlab-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = {"test:": "data"}
+ response = client.post(url,
+ json.dumps(data),
+ content_type="application/json",
+ REMOTE_ADDR="111.111.111.111")
+
+ assert response.status_code == 451
+
+
def test_invalid_ip(client):
project = f.ProjectFactory()
f.ProjectModulesConfigFactory(project=project, config={
From 1f1b1b605ae5c33014a3db48a918d5ae3d25ffc6 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 28 Jan 2016 11:52:16 +0100
Subject: [PATCH 009/105] Blocking projects when users cancelling accounts
---
taiga/users/models.py | 4 ++++
tests/integration/test_users.py | 13 +++++++++++++
2 files changed, 17 insertions(+)
diff --git a/taiga/users/models.py b/taiga/users/models.py
index 238ffa00..e5624fd0 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -42,6 +42,7 @@ from taiga.auth.tokens import get_token_for_user
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.iterators import split_by_n
from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel
from easy_thumbnails.files import get_thumbnailer
@@ -245,6 +246,9 @@ class User(AbstractBaseUser, PermissionsMixin):
self.save()
self.auth_data.all().delete()
+ #Blocking all owned users
+ self.owned_projects.update(blocked_code=BLOCKED_BY_OWNER_LEAVING)
+
class Role(models.Model):
name = models.CharField(max_length=200, null=False, blank=False,
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 88b29523..0761b3bf 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -15,6 +15,7 @@ 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.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
from taiga.projects.notifications.models import NotifyPolicy
@@ -152,6 +153,18 @@ def test_delete_self_user(client):
assert user.full_name == "Deleted user"
+def test_delete_self_user_blocking_projects(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ url = reverse('users-detail', kwargs={"pk": user.pk})
+
+ assert project.blocked_code == None
+ client.login(user)
+ response = client.delete(url)
+ project = user.owned_projects.first()
+ assert project.blocked_code == project_choices.BLOCKED_BY_OWNER_LEAVING
+
+
def test_cancel_self_user_with_valid_token(client):
user = f.UserFactory.create()
url = reverse('users-cancel')
From b8acb0d51e8ff1017b06386ed1a1ff8cea644c2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 2 Feb 2016 11:43:32 +0100
Subject: [PATCH 010/105] Fix #3846: Exclude private projects in discover calls
---
taiga/projects/api.py | 5 ++--
taiga/projects/filters.py | 19 ++++++++++--
.../test_projects_resource.py | 30 +++++++++++++++++++
3 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 2346ae29..2b04085e 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -70,8 +70,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, )
- filter_backends = (project_filters.QFilter,
- project_filters.CanViewProjectObjFilterBackend)
+ filter_backends = (project_filters.QFilterBackend,
+ project_filters.CanViewProjectObjFilterBackend,
+ project_filters.DiscoverModeFilterBackend)
filter_fields = (("member", "members"),
"is_looking_for_people",
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index dbc5b557..5868a6be 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -27,6 +27,21 @@ from taiga.base.utils.db import to_tsquery
logger = logging.getLogger(__name__)
+class DiscoverModeFilterBackend(FilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ qs = queryset
+
+ if "discover_mode" in request.QUERY_PARAMS:
+ field_data = request.QUERY_PARAMS["discover_mode"]
+ discover_mode = self._special_values_dict.get(field_data, field_data)
+
+ if discover_mode:
+ # discover_mode enabled
+ qs = qs.filter(anon_permissions__contains=["view_project"])
+
+ return super().filter_queryset(request, qs.distinct(), view)
+
+
class CanViewProjectObjFilterBackend(FilterBackend):
def filter_queryset(self, request, queryset, view):
project_id = None
@@ -49,7 +64,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
# superuser
qs = qs
elif request.user.is_authenticated():
- # projet members
+ # authenticated user & project member
membership_model = apps.get_model("projects", "Membership")
memberships_qs = membership_model.objects.filter(user=request.user)
if project_id:
@@ -68,7 +83,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
return super().filter_queryset(request, qs.distinct(), view)
-class QFilter(FilterBackend):
+class QFilterBackend(FilterBackend):
def filter_queryset(self, request, queryset, view):
# NOTE: See migtration 0033_text_search_indexes
q = request.QUERY_PARAMS.get('q', None)
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index f90c10b7..f05b04e5 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -575,3 +575,33 @@ def test_project_action_unwatch(client, data):
assert results == [404, 404, 200, 200]
results = helper_test_http_method(client, 'post', blocked_url, None, users)
assert results == [404, 404, 451, 451]
+
+
+def test_project_list_with_discover_mode_enabled(client, data):
+ url = "{}?{}".format(reverse('projects-list'), "discover_mode=true")
+
+ response = client.get(url)
+ projects_data = json.loads(response.content.decode('utf-8'))
+ assert len(projects_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.registered_user)
+
+ response = client.get(url)
+ projects_data = json.loads(response.content.decode('utf-8'))
+ assert len(projects_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_member_with_perms)
+
+ response = client.get(url)
+ projects_data = json.loads(response.content.decode('utf-8'))
+ assert len(projects_data) == 2
+ assert response.status_code == 200
+
+ client.login(data.project_owner)
+
+ response = client.get(url)
+ projects_data = json.loads(response.content.decode('utf-8'))
+ assert len(projects_data) == 2
+ assert response.status_code == 200
From be0f5d370654569b2d8ec7aea0d83beb15a3bb35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 2 Feb 2016 19:33:26 +0100
Subject: [PATCH 011/105] Issue #3856: Show if I am a member of a project
---
taiga/projects/serializers.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 70875cd4..54d6a48b 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -312,6 +312,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
public_permissions = PgArrayField(required=False)
my_permissions = serializers.SerializerMethodField("get_my_permissions")
i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
+ i_am_member = serializers.SerializerMethodField("get_i_am_member")
tags = TagsField(default=[], required=False)
tags_colors = TagsColorsField(required=False)
@@ -339,6 +340,13 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return is_project_owner(self.context["request"].user, obj)
return False
+ def get_i_am_member(self, obj):
+ if "request" in self.context:
+ user = self.context["request"].user
+ if not user.is_anonymous() and user.cached_membership_for_project(obj):
+ return True
+ return False
+
def get_total_closed_milestones(self, obj):
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
qs_closed_milestones = getattr(obj, "closed_milestones", None)
From 22993203e89c6304f01539031f3b6a9e69f918b7 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 3 Feb 2016 08:36:47 +0100
Subject: [PATCH 012/105] Fixing duplicated wiki page slugs
---
taiga/projects/wiki/models.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py
index 86bb15a8..abbdf44d 100644
--- a/taiga/projects/wiki/models.py
+++ b/taiga/projects/wiki/models.py
@@ -20,7 +20,7 @@ from django.contrib.contenttypes import generic
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
-from taiga.base.utils.slug import slugify
+from taiga.base.utils.slug import slugify_uniquely_for_queryset
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.occ import OCCModelMixin
@@ -83,6 +83,7 @@ class WikiLink(models.Model):
def save(self, *args, **kwargs):
if not self.href:
- self.href = slugify(self.title)
+ wl_qs = self.project.wiki_links.all()
+ self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href")
super().save(*args, **kwargs)
From 57b9f68c5dc28440a54207f56b1232e8e1aa899d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 3 Feb 2016 13:29:21 +0100
Subject: [PATCH 013/105] Update CHECKOUT
---
CHANGELOG.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a570ed3d..28ae893e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,15 @@
# Changelog #
+## 2.0.0 ??? (unreleased)
+
+### Features
+- ...
+
+### Misc
+- Lots of small and not so small bugfixes.
+
+
## 1.10.0 Dryas Octopetala (2016-01-30)
### Features
From b0a02ba4011fca3c3feddd33fb3215555feddf6e Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 4 Feb 2016 15:02:40 +0100
Subject: [PATCH 014/105] Trying to fix random signal errors
---
taiga/events/signal_handlers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/taiga/events/signal_handlers.py b/taiga/events/signal_handlers.py
index 4c7c8928..360e780d 100644
--- a/taiga/events/signal_handlers.py
+++ b/taiga/events/signal_handlers.py
@@ -55,5 +55,5 @@ def on_delete_any_model(sender, instance, **kwargs):
return
sesionid = mw.get_current_session_id()
- emit_event = lambda: events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
- connection.on_commit(emit_event)
+ events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
+
From 273d94f347eeac4e9fe241597f2acf21be89cbfa Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 4 Feb 2016 15:18:08 +0100
Subject: [PATCH 015/105] Updating CHANGELOG: including blocking projects
feature
---
CHANGELOG.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28ae893e..961dad9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
## 2.0.0 ??? (unreleased)
### Features
-- ...
+- Blocked projects support
### Misc
- Lots of small and not so small bugfixes.
@@ -19,7 +19,7 @@
- Filter projects list by
- is_looking_for_people
- is_featured
- - is_backlog_activated
+ - is_backlog_activated
- is_kanban_activated
- Search projects by text query (order by ranking name > tags > description)
- Order projects list:
From b8fd768d0194e9971e676c1ea0e6dfd2bcd65066 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 20 Jan 2016 15:13:22 +0100
Subject: [PATCH 016/105] Adding max_private_projects and max_public_projects
support
---
settings/common.py | 3 +
taiga/projects/api.py | 8 +-
taiga/users/admin.py | 2 +-
.../migrations/0015_auto_20160120_1409.py | 24 ++++
taiga/users/models.py | 6 +
taiga/users/serializers.py | 17 ++-
taiga/users/services.py | 13 ++
tests/integration/test_projects.py | 124 ++++++++++++++++++
8 files changed, 192 insertions(+), 5 deletions(-)
create mode 100644 taiga/users/migrations/0015_auto_20160120_1409.py
diff --git a/settings/common.py b/settings/common.py
index e013b3f7..fa1f9d73 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -524,6 +524,9 @@ FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
EXTRA_BLOCKING_CODES = []
+MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
+MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
+
from .sr import *
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 2b04085e..15017817 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -51,6 +51,7 @@ 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 . import filters as project_filters
from . import models
@@ -342,9 +343,12 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
permissions_service.set_base_permissions_for_project(obj)
def pre_save(self, obj):
+ user = self.request.user
+ if not users_service.has_available_slot_for_project(user, is_private=obj.is_private):
+ raise exc.BadRequest(_("The user can't have more projects of this type"))
+
if not obj.id:
- obj.owner = self.request.user
- # TODO REFACTOR THIS
+ obj.owner = user
obj.template = self.request.QUERY_PARAMS.get('template', None)
self._set_base_permissions(obj)
diff --git a/taiga/users/admin.py b/taiga/users/admin.py
index 729d64bf..d5a3a4a0 100644
--- a/taiga/users/admin.py
+++ b/taiga/users/admin.py
@@ -50,7 +50,7 @@ class UserAdmin(DjangoUserAdmin):
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
(_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
- (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}),
+ (_('Permissions'), {'fields': ('is_active', 'is_superuser', 'max_private_projects', 'max_public_projects')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
form = UserChangeForm
diff --git a/taiga/users/migrations/0015_auto_20160120_1409.py b/taiga/users/migrations/0015_auto_20160120_1409.py
new file mode 100644
index 00000000..3fea9616
--- /dev/null
+++ b/taiga/users/migrations/0015_auto_20160120_1409.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0014_auto_20151005_1357'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='max_private_projects',
+ field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=None, blank=True),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='max_public_projects',
+ field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=None, blank=True),
+ ),
+ ]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index e5624fd0..9ce48998 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -25,6 +25,7 @@ import uuid
from unidecode import unidecode
from django.apps import apps
+from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.dispatch import receiver
@@ -140,6 +141,11 @@ class User(AbstractBaseUser, PermissionsMixin):
new_email = models.EmailField(_('new email address'), null=True, blank=True)
is_system = models.BooleanField(null=False, blank=False, default=False)
+
+
+ max_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PRIVATE_PROJECTS_PER_USER, verbose_name='max number of private projects owned')
+ max_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PUBLIC_PROJECTS_PER_USER, verbose_name='max number of public projects owned')
+
_cached_memberships = None
_cached_liked_ids = None
_cached_watched_ids = None
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 5b381252..bfceda55 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -104,14 +104,27 @@ class UserSerializer(serializers.ModelSerializer):
return ContactProjectDetailSerializer(projects, many=True).data
class UserAdminSerializer(UserSerializer):
+ total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
+ total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
+
class Meta:
model = User
# IMPORTANT: Maintain the UserSerializer Meta up to date
# with this info (including here the email)
fields = ("id", "username", "full_name", "full_name_display", "email",
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
- "big_photo")
- read_only_fields = ("id", "email")
+ "big_photo",
+ "max_private_projects", "max_public_projects",
+ "total_private_projects", "total_public_projects")
+
+ read_only_fields = ("id", "email",
+ "max_private_projects", "max_public_projects")
+
+ def get_total_private_projects(self, user):
+ return user.owned_projects.filter(is_private=True).count()
+
+ def get_total_public_projects(self, user):
+ return user.owned_projects.filter(is_private=False).count()
class UserBasicInfoSerializer(UserSerializer):
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 55c82ca1..45dd4be9 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -572,3 +572,16 @@ def get_voted_list(for_user, from_user, type=None, q=None):
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]
+
+
+def has_available_slot_for_project(user, is_private=False):
+ if is_private:
+ if user.max_private_projects is None:
+ return True
+
+ return user.owned_projects.filter(is_private=True).count() < user.max_private_projects
+
+ if user.max_public_projects is None:
+ return True
+
+ return user.owned_projects.filter(is_private=False).count() < user.max_public_projects
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 8b079fea..603b327a 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -43,6 +43,130 @@ def test_create_project(client):
assert response.status_code == 201
+def test_create_private_project_without_enough_private_projects_slots(client):
+ user = f.create_user(max_private_projects=0)
+ url = reverse("projects-list")
+ data = {
+ "name": "project name",
+ "description": "project description",
+ "is_private": True
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+
+
+def test_create_public_project_without_enough_public_projects_slots(client):
+ user = f.create_user(max_public_projects=0)
+ url = reverse("projects-list")
+ data = {
+ "name": "project name",
+ "description": "project description",
+ "is_private": False
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+
+
+def test_change_project_from_private_to_public_without_enough_public_projects_slots(client):
+ project = f.create_project(is_private=True, owner__max_public_projects=0)
+ f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ url = reverse("projects-detail", kwargs={"pk": project.pk})
+
+ data = {
+ "is_private": False
+ }
+
+ client.login(project.owner)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+
+
+def test_change_project_from_public_to_private_without_enough_private_projects_slots(client):
+ project = f.create_project(is_private=False, owner__max_private_projects=0)
+ f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ url = reverse("projects-detail", kwargs={"pk": project.pk})
+
+ data = {
+ "is_private": True
+ }
+
+ client.login(project.owner)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+
+
+def test_create_private_project_with_enough_private_projects_slots(client):
+ user = f.create_user(max_private_projects=1)
+ url = reverse("projects-list")
+ data = {
+ "name": "project name",
+ "description": "project description",
+ "is_private": True
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 201
+
+
+def test_create_public_project_with_enough_public_projects_slots(client):
+ user = f.create_user(max_public_projects=1)
+ url = reverse("projects-list")
+ data = {
+ "name": "project name",
+ "description": "project description",
+ "is_private": False
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 201
+
+
+def test_change_project_from_private_to_public_with_enough_public_projects_slots(client):
+ project = f.create_project(is_private=True, owner__max_public_projects=1)
+ f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ url = reverse("projects-detail", kwargs={"pk": project.pk})
+
+ data = {
+ "is_private": False
+ }
+
+ client.login(project.owner)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
+def test_change_project_from_public_to_private_with_enough_private_projects_slots(client):
+ project = f.create_project(is_private=False, owner__max_private_projects=1)
+ f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ url = reverse("projects-detail", kwargs={"pk": project.pk})
+
+ data = {
+ "is_private": True
+ }
+
+ client.login(project.owner)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
def test_partially_update_project(client):
project = f.create_project()
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
From 6fbf81c3d6c0cadec8db10ce87a07f3c79e6ff7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Wed, 20 Jan 2016 16:46:07 +0100
Subject: [PATCH 017/105] Including max_public_projects and max_private_projets
in importing process
---
taiga/export_import/api.py | 14 +-
taiga/export_import/dump_service.py | 5 +-
.../management/commands/load_dump.py | 6 +-
taiga/export_import/tasks.py | 2 +-
tests/integration/test_importer_api.py | 148 +++++++++++++++++-
5 files changed, 165 insertions(+), 10 deletions(-)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index cdfc36f0..6f20cc60 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -36,6 +36,7 @@ from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.serializers import ProjectSerializer
+from taiga.users import services as users_service
from . import mixins
from . import serializers
@@ -90,6 +91,10 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
data = request.DATA.copy()
data['owner'] = data.get('owner', request.user.email)
+ is_private = data.get('is_private', False)
+ if not users_service.has_available_slot_for_project(self.request.user, is_private=is_private):
+ raise exc.BadRequest(_("The user can't have more projects of this type"))
+
# Create Project
project_serialized = service.store_project(data)
@@ -202,17 +207,22 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
try:
dump = json.load(reader(dump))
+ is_private = dump["is_private"]
except Exception:
raise exc.WrongArguments(_("Invalid dump format"))
if Project.objects.filter(slug=dump['slug']).exists():
del dump['slug']
+ user = request.user
+ if not users_service.has_available_slot_for_project(user, is_private=is_private):
+ raise exc.BadRequest(_("The user can't have more projects of this type"))
+
if settings.CELERY_ENABLED:
- task = tasks.load_project_dump.delay(request.user, dump)
+ task = tasks.load_project_dump.delay(user, dump)
return response.Accepted({"import_id": task.id})
- project = dump_service.dict_to_project(dump, request.user.email)
+ project = dump_service.dict_to_project(dump, request.user)
response_data = ProjectSerializer(project).data
return response.Created(response_data)
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index 8029fa0f..0237145a 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -18,6 +18,7 @@
from django.utils.translation import ugettext as _
from taiga.projects.models import Membership
+from taiga.users import services as users_service
from . import serializers
from . import service
@@ -89,7 +90,9 @@ def store_tags_colors(project, data):
def dict_to_project(data, owner=None):
if owner:
- data["owner"] = owner
+ data["owner"] = owner.email
+ if not users_service.has_available_slot_for_project(owner, is_private=data["is_private"]):
+ raise TaigaImportError(_("The user can't have more projects of this type"))
project_serialized = service.store_project(data)
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index 1b44adbf..bea52417 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -25,7 +25,7 @@ from taiga.projects.models import Project
from taiga.export_import.renderers import ExportRenderer
from taiga.export_import.dump_service import dict_to_project, TaigaImportError
from taiga.export_import.service import get_errors
-
+from taiga.users.models import User
class Command(BaseCommand):
args = ' '
@@ -58,7 +58,9 @@ class Command(BaseCommand):
except Project.DoesNotExist:
pass
signals.post_delete.receivers = receivers_back
- dict_to_project(data, args[1])
+
+ user = User.objects.get(email=args[1])
+ dict_to_project(data, user)
except TaigaImportError as e:
print("ERROR:", end=" ")
print(e.message)
diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py
index c6389b8b..8044f35c 100644
--- a/taiga/export_import/tasks.py
+++ b/taiga/export_import/tasks.py
@@ -79,7 +79,7 @@ def delete_project_dump(project_id, project_slug, task_id):
@app.task
def load_project_dump(user, dump):
try:
- project = dict_to_project(dump, user.email)
+ project = dict_to_project(dump, user)
except Exception:
ctx = {
"user": user,
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 360550fb..996e45c6 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -23,6 +23,7 @@ from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from taiga.base.utils import json
+from taiga.export_import.dump_service import dict_to_project, TaigaImportError
from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.userstories.models import UserStory
@@ -72,6 +73,85 @@ def test_valid_project_import_without_extra_data(client):
assert response_data["watchers"] == [user.email, user_watching.email]
+def test_valid_project_without_enough_public_projects_slots(client):
+ user = f.UserFactory.create(max_public_projects=0)
+
+ url = reverse("importer-list")
+ data = {
+ "slug": "public-project-without-slots",
+ "name": "Imported project",
+ "description": "Imported project",
+ "roles": [{"name": "Role"}],
+ "is_private": False
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+ assert Project.objects.filter(slug="public-project-without-slots").count() == 0
+
+
+def test_valid_project_without_enough_private_projects_slots(client):
+ user = f.UserFactory.create(max_private_projects=0)
+
+ url = reverse("importer-list")
+ data = {
+ "slug": "private-project-without-slots",
+ "name": "Imported project",
+ "description": "Imported project",
+ "roles": [{"name": "Role"}],
+ "is_private": True
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+ assert Project.objects.filter(slug="private-project-without-slots").count() == 0
+
+
+def test_valid_project_with_enough_public_projects_slots(client):
+ user = f.UserFactory.create(max_public_projects=1)
+
+ url = reverse("importer-list")
+ data = {
+ "slug": "public-project-with-slots",
+ "name": "Imported project",
+ "description": "Imported project",
+ "roles": [{"name": "Role"}],
+ "is_private": False
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ print(response.content)
+ assert response.status_code == 201
+ assert Project.objects.filter(slug="public-project-with-slots").count() == 1
+
+
+def test_valid_project_with_enough_private_projects_slots(client):
+ user = f.UserFactory.create(max_private_projects=1)
+
+ url = reverse("importer-list")
+ data = {
+ "slug": "private-project-with-slots",
+ "name": "Imported project",
+ "description": "Imported project",
+ "roles": [{"name": "Role"}],
+ "is_private": True
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 201
+ assert Project.objects.filter(slug="private-project-with-slots").count() == 1
+
+
def test_valid_project_import_with_not_existing_memberships(client):
user = f.UserFactory.create()
client.login(user)
@@ -930,6 +1010,22 @@ def test_milestone_import_duplicated_milestone(client):
assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project"
+def test_dict_to_project_with_no_slots_available(client):
+ user = f.UserFactory.create(max_private_projects=0)
+
+ data = {
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True
+ }
+
+ with pytest.raises(TaigaImportError) as excinfo:
+ project = dict_to_project(data, owner=user)
+
+ assert "can't have more projects" in str(excinfo.value)
+
+
def test_invalid_dump_import(client):
user = f.UserFactory.create()
client.login(user)
@@ -986,7 +1082,8 @@ def test_valid_dump_import_with_celery_disabled(client, settings):
data = ContentFile(bytes(json.dumps({
"slug": "valid-project",
"name": "Valid project",
- "description": "Valid project desc"
+ "description": "Valid project desc",
+ "is_private": True
}), "utf-8"))
data.name = "test"
@@ -1008,7 +1105,8 @@ def test_valid_dump_import_with_celery_enabled(client, settings):
data = ContentFile(bytes(json.dumps({
"slug": "valid-project",
"name": "Valid project",
- "description": "Valid project desc"
+ "description": "Valid project desc",
+ "is_private": True
}), "utf-8"))
data.name = "test"
@@ -1028,7 +1126,8 @@ def test_dump_import_duplicated_project(client):
data = ContentFile(bytes(json.dumps({
"slug": project.slug,
"name": "Test import",
- "description": "Valid project desc"
+ "description": "Valid project desc",
+ "is_private": True
}), "utf-8"))
data.name = "test"
@@ -1051,7 +1150,8 @@ def test_dump_import_throttling(client, settings):
data = ContentFile(bytes(json.dumps({
"slug": project.slug,
"name": "Test import",
- "description": "Valid project desc"
+ "description": "Valid project desc",
+ "is_private": True
}), "utf-8"))
data.name = "test"
@@ -1059,3 +1159,43 @@ def test_dump_import_throttling(client, settings):
assert response.status_code == 201
response = client.post(url, {'dump': data})
assert response.status_code == 429
+
+
+def test_valid_dump_import_without_enough_public_projects_slots(client):
+ user = f.UserFactory.create(max_public_projects=0)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "public-project-without-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+ assert Project.objects.filter(slug="public-project-without-slots").count() == 0
+
+
+def test_valid_dump_import_without_enough_private_projects_slots(client):
+ user = f.UserFactory.create(max_private_projects=0)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "private-project-without-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 400
+ assert "can't have more projects" in response.data["_error_message"]
+ assert Project.objects.filter(slug="private-project-without-slots").count() == 0
From 9fd896c948d59d6ce8bbd1cdddb9b2567f4df1b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Mon, 25 Jan 2016 12:25:06 +0100
Subject: [PATCH 018/105] Adding limit for user max memberhips for projects
---
settings/common.py | 2 +
taiga/export_import/api.py | 35 ++-
taiga/export_import/dump_service.py | 12 +-
.../management/commands/load_dump.py | 1 +
taiga/projects/api.py | 23 +-
taiga/users/admin.py | 29 +-
.../migrations/0015_auto_20160120_1409.py | 5 +-
.../migrations/0016_auto_20160204_1050.py | 25 ++
taiga/users/models.py | 58 ++--
taiga/users/serializers.py | 5 +-
taiga/users/services.py | 41 ++-
tests/integration/test_importer_api.py | 289 +++++++++++++++++-
tests/integration/test_memberships.py | 176 +++++++++++
tests/integration/test_projects.py | 23 +-
14 files changed, 655 insertions(+), 69 deletions(-)
create mode 100644 taiga/users/migrations/0016_auto_20160204_1050.py
diff --git a/settings/common.py b/settings/common.py
index fa1f9d73..7096f6e6 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -526,6 +526,8 @@ EXTRA_BLOCKING_CODES = []
MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
+MAX_MEMBERS_PRIVATE_PROJECTS = None # None == no limit
+MAX_MEMBERS_PUBLIC_PROJECTS = None # None == no limit
from .sr import *
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index 6f20cc60..af472e3d 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -92,8 +92,12 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
data['owner'] = data.get('owner', request.user.email)
is_private = data.get('is_private', False)
- if not users_service.has_available_slot_for_project(self.request.user, is_private=is_private):
- raise exc.BadRequest(_("The user can't have more projects of this type"))
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ project=Project(is_private=is_private, id=None)
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
# Create Project
project_serialized = service.store_project(data)
@@ -111,6 +115,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create memberships
if "memberships" in data:
+ members = len(data['memberships'])
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ project=Project(is_private=is_private, id=None),
+ members=max(members, 1)
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
service.store_memberships(project_serialized.object, data)
try:
@@ -211,12 +223,25 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
except Exception:
raise exc.WrongArguments(_("Invalid dump format"))
+ user = request.user
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ user,
+ project=Project(is_private=is_private, id=None)
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
+
if Project.objects.filter(slug=dump['slug']).exists():
del dump['slug']
- user = request.user
- if not users_service.has_available_slot_for_project(user, is_private=is_private):
- raise exc.BadRequest(_("The user can't have more projects of this type"))
+ members = len(dump.get("memberships", []))
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ user,
+ project=Project(is_private=is_private, id=None),
+ members=max(members, 1)
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
if settings.CELERY_ENABLED:
task = tasks.load_project_dump.delay(user, dump)
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index 0237145a..adbaa6fe 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -17,7 +17,7 @@
from django.utils.translation import ugettext as _
-from taiga.projects.models import Membership
+from taiga.projects.models import Membership, Project
from taiga.users import services as users_service
from . import serializers
@@ -91,8 +91,14 @@ def store_tags_colors(project, data):
def dict_to_project(data, owner=None):
if owner:
data["owner"] = owner.email
- if not users_service.has_available_slot_for_project(owner, is_private=data["is_private"]):
- raise TaigaImportError(_("The user can't have more projects of this type"))
+ members = len(data.get("memberships", []))
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ owner,
+ project=Project(is_private=data["is_private"], id=None),
+ members=members
+ )
+ if not enough_slots:
+ raise TaigaImportError(not_enough_slots_error)
project_serialized = service.store_project(data)
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index bea52417..367a2401 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -27,6 +27,7 @@ from taiga.export_import.dump_service import dict_to_project, TaigaImportError
from taiga.export_import.service import get_errors
from taiga.users.models import User
+
class Command(BaseCommand):
args = ' '
help = 'Export a project to json'
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 15017817..a5e2838c 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -344,8 +344,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
def pre_save(self, obj):
user = self.request.user
- if not users_service.has_available_slot_for_project(user, is_private=obj.is_private):
- raise exc.BadRequest(_("The user can't have more projects of this type"))
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(user, project=obj)
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
if not obj.id:
obj.owner = user
@@ -554,6 +555,15 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
# TODO: this should be moved to main exception handler instead
# of handling explicit exception catchin here.
+ if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ request.user,
+ project=project,
+ members=len(data["bulk_memberships"])
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
+
try:
members = services.create_members_in_bulk(data["bulk_memberships"],
project=project,
@@ -581,6 +591,15 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin"))
def pre_save(self, obj):
+ if not obj.id:
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ project=obj.project,
+ members=1
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
+
if not obj.token:
obj.token = str(uuid.uuid1())
diff --git a/taiga/users/admin.py b/taiga/users/admin.py
index d5a3a4a0..76bc701d 100644
--- a/taiga/users/admin.py
+++ b/taiga/users/admin.py
@@ -30,14 +30,14 @@ admin.site.unregister(Group)
class RoleAdmin(admin.ModelAdmin):
list_display = ["name"]
- filter_horizontal = ('permissions',)
+ filter_horizontal = ("permissions",)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
- if db_field.name == 'permissions':
- qs = kwargs.get('queryset', db_field.rel.to.objects)
+ if db_field.name == "permissions":
+ qs = kwargs.get("queryset", db_field.rel.to.objects)
# Avoid a major performance hit resolving permission names which
# triggers a content_type load:
- kwargs['queryset'] = qs.select_related('content_type')
+ kwargs["queryset"] = qs.select_related("content_type")
return super().formfield_for_manytomany(
db_field, request=request, **kwargs)
@@ -47,18 +47,21 @@ class RoleAdmin(admin.ModelAdmin):
class UserAdmin(DjangoUserAdmin):
fieldsets = (
- (None, {'fields': ('username', 'password')}),
- (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
- (_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
- (_('Permissions'), {'fields': ('is_active', 'is_superuser', 'max_private_projects', 'max_public_projects')}),
- (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
+ (None, {"fields": ("username", "password")}),
+ (_("Personal info"), {"fields": ("full_name", "email", "bio", "photo")}),
+ (_("Extra info"), {"fields": ("color", "lang", "timezone", "token", "colorize_tags",
+ "email_token", "new_email")}),
+ (_("Permissions"), {"fields": ("is_active", "is_superuser")}),
+ (_("Restrictions"), {"fields": (("max_private_projects", "max_members_private_projects"),
+ ("max_public_projects", "max_members_public_projects"))}),
+ (_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
form = UserChangeForm
add_form = UserCreationForm
- list_display = ('username', 'email', 'full_name')
- list_filter = ('is_superuser', 'is_active')
- search_fields = ('username', 'full_name', 'email')
- ordering = ('username',)
+ list_display = ("username", "email", "full_name")
+ list_filter = ("is_superuser", "is_active")
+ search_fields = ("username", "full_name", "email")
+ ordering = ("username",)
filter_horizontal = ()
class RoleInline(admin.TabularInline):
diff --git a/taiga/users/migrations/0015_auto_20160120_1409.py b/taiga/users/migrations/0015_auto_20160120_1409.py
index 3fea9616..2b62b760 100644
--- a/taiga/users/migrations/0015_auto_20160120_1409.py
+++ b/taiga/users/migrations/0015_auto_20160120_1409.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
+from django.conf import settings
class Migration(migrations.Migration):
@@ -14,11 +15,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='user',
name='max_private_projects',
- field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=None, blank=True),
+ field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=settings.MAX_PRIVATE_PROJECTS_PER_USER, blank=True),
),
migrations.AddField(
model_name='user',
name='max_public_projects',
- field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=None, blank=True),
+ field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=settings.MAX_PUBLIC_PROJECTS_PER_USER, blank=True),
),
]
diff --git a/taiga/users/migrations/0016_auto_20160204_1050.py b/taiga/users/migrations/0016_auto_20160204_1050.py
new file mode 100644
index 00000000..d148f56a
--- /dev/null
+++ b/taiga/users/migrations/0016_auto_20160204_1050.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0015_auto_20160120_1409'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='max_members_private_projects',
+ field=models.IntegerField(default=settings.MAX_MEMBERS_PRIVATE_PROJECTS, blank=True, verbose_name='max number of memberships for each owned private project', null=True),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='max_members_public_projects',
+ field=models.IntegerField(default=settings.MAX_MEMBERS_PUBLIC_PROJECTS, blank=True, verbose_name='max number of memberships for each owned public project', null=True),
+ ),
+ ]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index 9ce48998..e55bf72c 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -72,11 +72,11 @@ def get_user_file_path(instance, filename):
class PermissionsMixin(models.Model):
"""
A mixin class that adds the fields and methods necessary to support
- Django's Permission model using the ModelBackend.
+ Django"s Permission model using the ModelBackend.
"""
- is_superuser = models.BooleanField(_('superuser status'), default=False,
- help_text=_('Designates that this user has all permissions without '
- 'explicitly assigning them.'))
+ is_superuser = models.BooleanField(_("superuser status"), default=False,
+ help_text=_("Designates that this user has all permissions without "
+ "explicitly assigning them."))
class Meta:
abstract = True
@@ -105,25 +105,25 @@ class PermissionsMixin(models.Model):
class User(AbstractBaseUser, PermissionsMixin):
- username = models.CharField(_('username'), max_length=255, unique=True,
- help_text=_('Required. 30 characters or fewer. Letters, numbers and '
- '/./-/_ characters'),
+ username = models.CharField(_("username"), max_length=255, unique=True,
+ help_text=_("Required. 30 characters or fewer. Letters, numbers and "
+ "/./-/_ characters"),
validators=[
- validators.RegexValidator(re.compile('^[\w.-]+$'), _('Enter a valid username.'), 'invalid')
+ validators.RegexValidator(re.compile("^[\w.-]+$"), _("Enter a valid username."), "invalid")
])
- email = models.EmailField(_('email address'), max_length=255, blank=True, unique=True)
- is_active = models.BooleanField(_('active'), default=True,
- help_text=_('Designates whether this user should be treated as '
- 'active. Unselect this instead of deleting accounts.'))
+ email = models.EmailField(_("email address"), max_length=255, blank=True, unique=True)
+ is_active = models.BooleanField(_("active"), default=True,
+ help_text=_("Designates whether this user should be treated as "
+ "active. Unselect this instead of deleting accounts."))
- full_name = models.CharField(_('full name'), max_length=256, blank=True)
+ full_name = models.CharField(_("full name"), max_length=256, blank=True)
color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color,
verbose_name=_("color"))
bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography"))
photo = models.FileField(upload_to=get_user_file_path,
max_length=500, null=True, blank=True,
verbose_name=_("photo"))
- date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
+ date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
lang = models.CharField(max_length=20, null=True, blank=True, default="",
verbose_name=_("default language"))
theme = models.CharField(max_length=100, null=True, blank=True, default="",
@@ -138,21 +138,33 @@ class User(AbstractBaseUser, PermissionsMixin):
email_token = models.CharField(max_length=200, null=True, blank=True, default=None,
verbose_name=_("email token"))
- new_email = models.EmailField(_('new email address'), null=True, blank=True)
+ new_email = models.EmailField(_("new email address"), null=True, blank=True)
is_system = models.BooleanField(null=False, blank=False, default=False)
- max_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PRIVATE_PROJECTS_PER_USER, verbose_name='max number of private projects owned')
- max_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PUBLIC_PROJECTS_PER_USER, verbose_name='max number of public projects owned')
+ max_private_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_PRIVATE_PROJECTS_PER_USER,
+ verbose_name=_("max number of private projects owned"))
+ max_public_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_PUBLIC_PROJECTS_PER_USER,
+ verbose_name=_("max number of public projects owned"))
+ max_members_private_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_MEMBERS_PRIVATE_PROJECTS,
+ verbose_name=_("max number of memberships for "
+ "each owned private project"))
+ max_members_public_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_MEMBERS_PUBLIC_PROJECTS,
+ verbose_name=_("max number of memberships for "
+ "each owned public project"))
_cached_memberships = None
_cached_liked_ids = None
_cached_watched_ids = None
_cached_notify_levels = None
- USERNAME_FIELD = 'username'
- REQUIRED_FIELDS = ['email']
+ USERNAME_FIELD = "username"
+ REQUIRED_FIELDS = ["email"]
objects = UserManager()
@@ -160,9 +172,6 @@ class User(AbstractBaseUser, PermissionsMixin):
verbose_name = "user"
verbose_name_plural = "users"
ordering = ["username"]
- permissions = (
- ("view_user", "Can view user"),
- )
def __str__(self):
return self.get_full_name()
@@ -285,16 +294,13 @@ class Role(models.Model):
verbose_name_plural = "roles"
ordering = ["order", "slug"]
unique_together = (("slug", "project"),)
- permissions = (
- ("view_role", "Can view role"),
- )
def __str__(self):
return self.name
class AuthData(models.Model):
- user = models.ForeignKey('users.User', related_name="auth_data")
+ user = models.ForeignKey("users.User", related_name="auth_data")
key = models.SlugField(max_length=50)
value = models.CharField(max_length=300)
extra = JsonField()
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index bfceda55..204852ad 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -115,10 +115,13 @@ class UserAdminSerializer(UserSerializer):
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
"big_photo",
"max_private_projects", "max_public_projects",
+ "max_members_private_projects", "max_members_public_projects",
"total_private_projects", "total_public_projects")
read_only_fields = ("id", "email",
- "max_private_projects", "max_public_projects")
+ "max_private_projects", "max_public_projects",
+ "max_members_private_projects",
+ "max_members_public_projects")
def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count()
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 45dd4be9..54ce79d7 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -574,14 +574,41 @@ def get_voted_list(for_user, from_user, type=None, q=None):
]
-def has_available_slot_for_project(user, is_private=False):
- if is_private:
+def has_available_slot_for_project(user, project, members=1):
+ (enough, error) = _has_available_slot_for_project_type(user, project)
+ if not enough:
+ return (enough, error)
+ return _has_available_slot_for_project_members(user, project, members)
+
+
+def _has_available_slot_for_project_type(user, project):
+ if project.is_private:
if user.max_private_projects is None:
- return True
+ return (True, None)
+ elif user.owned_projects.filter(is_private=True).exclude(id=project.id).count() < user.max_private_projects:
+ return (True, None)
+ return (False, _("You can't have more private projects"))
+ else:
+ if user.max_public_projects is None:
+ return (True, None)
+ elif user.owned_projects.filter(is_private=False).exclude(id=project.id).count() < user.max_public_projects:
+ return (True, None)
+ return (False, _("You can't have more public projects"))
- return user.owned_projects.filter(is_private=True).count() < user.max_private_projects
- if user.max_public_projects is None:
- return True
- return user.owned_projects.filter(is_private=False).count() < user.max_public_projects
+def _has_available_slot_for_project_members(user, project, members):
+ current_memberships = project.memberships.count()
+
+ if project.is_private:
+ if user.max_members_private_projects is None:
+ return (True, None)
+ elif current_memberships + members <= user.max_members_private_projects:
+ return (True, None)
+ return (False, _("You have reached the limit of memberships for private projects"))
+ else:
+ if user.max_members_public_projects is None:
+ return (True, None)
+ elif current_memberships + members <= user.max_members_public_projects:
+ return (True, None)
+ return (False, _("You have reached the limit of memberships for public projects"))
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 996e45c6..52784d47 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -89,7 +89,7 @@ def test_valid_project_without_enough_public_projects_slots(client):
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more public projects" in response.data["_error_message"]
assert Project.objects.filter(slug="public-project-without-slots").count() == 0
@@ -109,7 +109,7 @@ def test_valid_project_without_enough_private_projects_slots(client):
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more private projects" in response.data["_error_message"]
assert Project.objects.filter(slug="private-project-without-slots").count() == 0
@@ -1010,7 +1010,7 @@ def test_milestone_import_duplicated_milestone(client):
assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project"
-def test_dict_to_project_with_no_slots_available(client):
+def test_dict_to_project_with_no_projects_slots_available(client):
user = f.UserFactory.create(max_private_projects=0)
data = {
@@ -1023,7 +1023,77 @@ def test_dict_to_project_with_no_slots_available(client):
with pytest.raises(TaigaImportError) as excinfo:
project = dict_to_project(data, owner=user)
- assert "can't have more projects" in str(excinfo.value)
+ assert "can't have more private projects" in str(excinfo.value)
+
+
+def test_dict_to_project_with_no_members_private_project_slots_available(client):
+ user = f.UserFactory.create(max_members_private_projects=2)
+
+ data = {
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True,
+ "roles": [{"name": "Role"}],
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ }
+ ]
+ }
+
+ with pytest.raises(TaigaImportError) as excinfo:
+ project = dict_to_project(data, owner=user)
+
+ assert "reached the limit of memberships for private" in str(excinfo.value)
+
+
+def test_dict_to_project_with_no_members_public_project_slots_available(client):
+ user = f.UserFactory.create(max_members_public_projects=2)
+
+ data = {
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "roles": [{"name": "Role"}],
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ }
+ ]
+ }
+
+ with pytest.raises(TaigaImportError) as excinfo:
+ project = dict_to_project(data, owner=user)
+
+ assert "reached the limit of memberships for public" in str(excinfo.value)
def test_invalid_dump_import(client):
@@ -1053,6 +1123,7 @@ def test_valid_dump_import_with_logo(client, settings):
"slug": "valid-project",
"name": "Valid project",
"description": "Valid project desc",
+ "is_private": False,
"logo": {
"name": "logo.bmp",
"data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8")
@@ -1177,7 +1248,7 @@ def test_valid_dump_import_without_enough_public_projects_slots(client):
response = client.post(url, {'dump': data})
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more public projects" in response.data["_error_message"]
assert Project.objects.filter(slug="public-project-without-slots").count() == 0
@@ -1197,5 +1268,211 @@ def test_valid_dump_import_without_enough_private_projects_slots(client):
response = client.post(url, {'dump': data})
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more private projects" in response.data["_error_message"]
assert Project.objects.filter(slug="private-project-without-slots").count() == 0
+
+
+def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client):
+ user = f.UserFactory.create(max_members_private_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "project-without-memberships-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True,
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test6@test.com",
+ "role": "Role",
+ },
+ ],
+ "roles": [{"name": "Role"}]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 400
+ assert "reached the limit of memberships for private" in response.data["_error_message"]
+ assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0
+
+
+def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client):
+ user = f.UserFactory.create(max_members_public_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "project-without-memberships-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test6@test.com",
+ "role": "Role",
+ },
+ ],
+ "roles": [{"name": "Role"}]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 400
+ assert "reached the limit of memberships for public" in response.data["_error_message"]
+ assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0
+
+
+def test_valid_dump_import_with_enough_membership_private_project_slots_multiple_projects(client, settings):
+ settings.CELERY_ENABLED = False
+
+ user = f.UserFactory.create(max_members_private_projects=10)
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "project-without-memberships-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True,
+ "roles": [{"name": "Role"}],
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test6@test.com",
+ "role": "Role",
+ }
+ ]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ response_data = response.data
+ assert "id" in response_data
+ assert response_data["name"] == "Valid project"
+
+
+def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_projects(client, settings):
+ settings.CELERY_ENABLED = False
+
+ user = f.UserFactory.create(max_members_public_projects=10)
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ f.MembershipFactory.create(project=project)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "project-without-memberships-slots",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "roles": [{"name": "Role"}],
+ "memberships": [
+ {
+ "email": "test1@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test6@test.com",
+ "role": "Role",
+ }
+ ]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ response_data = response.data
+ assert "id" in response_data
+ assert response_data["name"] == "Valid project"
diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py
index 75d7f29a..ec4e3124 100644
--- a/tests/integration/test_memberships.py
+++ b/tests/integration/test_memberships.py
@@ -53,6 +53,112 @@ def test_api_create_bulk_members(client):
assert response.data[1]["email"] == joseph.email
+def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client):
+ user = f.UserFactory.create(max_members_private_projects=3)
+ project = f.ProjectFactory(owner=user, is_private=True)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ url = reverse("memberships-bulk-create")
+
+ data = {
+ "project_id": project.id,
+ "bulk_memberships": [
+ {"role_id": role.pk, "email": "test1@test.com"},
+ {"role_id": role.pk, "email": "test2@test.com"},
+ {"role_id": role.pk, "email": "test3@test.com"},
+ {"role_id": role.pk, "email": "test4@test.com"},
+ ]
+ }
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "reached the limit of memberships for private" in response.data["_error_message"]
+
+
+def test_api_create_bulk_members_with_enough_memberships_private_project_slots_multiple_projects(client):
+ user = f.UserFactory.create(max_members_private_projects=6)
+ project = f.ProjectFactory(owner=user, is_private=True)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ other_project = f.ProjectFactory(owner=user)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+
+ url = reverse("memberships-bulk-create")
+
+ data = {
+ "project_id": project.id,
+ "bulk_memberships": [
+ {"role_id": role.pk, "email": "test1@test.com"},
+ {"role_id": role.pk, "email": "test2@test.com"},
+ {"role_id": role.pk, "email": "test3@test.com"},
+ {"role_id": role.pk, "email": "test4@test.com"},
+ ]
+ }
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
+def test_api_create_bulk_members_without_enough_memberships_public_project_slots_one_project(client):
+ user = f.UserFactory.create(max_members_public_projects=3)
+ project = f.ProjectFactory(owner=user, is_private=False)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ url = reverse("memberships-bulk-create")
+
+ data = {
+ "project_id": project.id,
+ "bulk_memberships": [
+ {"role_id": role.pk, "email": "test1@test.com"},
+ {"role_id": role.pk, "email": "test2@test.com"},
+ {"role_id": role.pk, "email": "test3@test.com"},
+ {"role_id": role.pk, "email": "test4@test.com"},
+ ]
+ }
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "reached the limit of memberships for public" in response.data["_error_message"]
+
+
+def test_api_create_bulk_members_with_enough_memberships_public_project_slots_multiple_projects(client):
+ user = f.UserFactory.create(max_members_public_projects=6)
+ project = f.ProjectFactory(owner=user, is_private=False)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ other_project = f.ProjectFactory(owner=user)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+
+ url = reverse("memberships-bulk-create")
+
+ data = {
+ "project_id": project.id,
+ "bulk_memberships": [
+ {"role_id": role.pk, "email": "test1@test.com"},
+ {"role_id": role.pk, "email": "test2@test.com"},
+ {"role_id": role.pk, "email": "test3@test.com"},
+ {"role_id": role.pk, "email": "test4@test.com"},
+ ]
+ }
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
def test_api_create_bulk_members_with_extra_text(client, outbox):
project = f.ProjectFactory()
tester = f.RoleFactory(project=project, name="Tester")
@@ -162,6 +268,76 @@ def test_api_create_membership(client):
assert response.data["user_email"] == user.email
+def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client):
+ user = f.UserFactory.create(max_members_private_projects=1)
+ project = f.ProjectFactory(owner=user, is_private=True)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ client.login(user)
+ url = reverse("memberships-list")
+ data = {"role": role.pk, "project": project.pk, "email": "test@test.com"}
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "reached the limit of memberships for private" in response.data["_error_message"]
+
+
+def test_api_create_membership_with_enough_memberships_private_project_slots_multiple_projects(client):
+ user = f.UserFactory.create(max_members_private_projects=5)
+ project = f.ProjectFactory(owner=user, is_private=True)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ other_project = f.ProjectFactory(owner=user)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+
+ client.login(user)
+ url = reverse("memberships-list")
+ data = {"role": role.pk, "project": project.pk, "email": "test@test.com"}
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 201
+
+
+def test_api_create_membership_without_enough_memberships_public_project_slots_one_projects(client):
+ user = f.UserFactory.create(max_members_public_projects=1)
+ project = f.ProjectFactory(owner=user, is_private=False)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ client.login(user)
+ url = reverse("memberships-list")
+ data = {"role": role.pk, "project": project.pk, "email": "test@test.com"}
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "reached the limit of memberships for public" in response.data["_error_message"]
+
+
+def test_api_create_membership_with_enough_memberships_public_project_slots_multiple_projects(client):
+ user = f.UserFactory.create(max_members_public_projects=5)
+ project = f.ProjectFactory(owner=user, is_private=False)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_owner=True)
+
+ other_project = f.ProjectFactory(owner=user)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+ f.MembershipFactory.create(project=other_project)
+
+ client.login(user)
+ url = reverse("memberships-list")
+ data = {"role": role.pk, "project": project.pk, "email": "test@test.com"}
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 201
+
+
def test_api_edit_membership(client):
membership = f.MembershipFactory(is_owner=True)
client.login(membership.user)
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 603b327a..1627d537 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -56,7 +56,7 @@ def test_create_private_project_without_enough_private_projects_slots(client):
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more private projects" in response.data["_error_message"]
def test_create_public_project_without_enough_public_projects_slots(client):
@@ -72,7 +72,7 @@ def test_create_public_project_without_enough_public_projects_slots(client):
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more public projects" in response.data["_error_message"]
def test_change_project_from_private_to_public_without_enough_public_projects_slots(client):
@@ -88,7 +88,7 @@ def test_change_project_from_private_to_public_without_enough_public_projects_sl
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more public projects" in response.data["_error_message"]
def test_change_project_from_public_to_private_without_enough_private_projects_slots(client):
@@ -104,7 +104,7 @@ def test_change_project_from_public_to_private_without_enough_private_projects_s
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
- assert "can't have more projects" in response.data["_error_message"]
+ assert "can't have more private projects" in response.data["_error_message"]
def test_create_private_project_with_enough_private_projects_slots(client):
@@ -167,6 +167,21 @@ def test_change_project_from_public_to_private_with_enough_private_projects_slot
assert response.status_code == 200
+def test_change_project_other_data_with_enough_private_projects_slots(client):
+ project = f.create_project(is_private=True, owner__max_private_projects=1)
+ f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ url = reverse("projects-detail", kwargs={"pk": project.pk})
+
+ data = {
+ "name": "test-project-change"
+ }
+
+ client.login(project.owner)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
def test_partially_update_project(client):
project = f.create_project()
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
From 57cf32c56df391fb50927c09bc7f9da9e9638318 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 21 Jan 2016 12:10:14 +0100
Subject: [PATCH 019/105] Adding transfer_token to project model
---
taiga/base/management/commands/test_emails.py | 35 ++
taiga/export_import/api.py | 2 +-
taiga/export_import/dump_service.py | 2 +-
taiga/front/urls.py | 2 +
taiga/permissions/service.py | 4 -
taiga/projects/api.py | 68 +++
.../migrations/0036_project_transfer_token.py | 19 +
taiga/projects/models.py | 3 +
taiga/projects/permissions.py | 13 +
taiga/projects/serializers.py | 3 +-
taiga/projects/services/__init__.py | 4 +
taiga/projects/services/transfer.py | 107 ++++
.../emails/transfer_accept-body-html.jinja | 19 +
.../emails/transfer_accept-body-text.jinja | 17 +
.../emails/transfer_accept-subject.jinja | 3 +
.../emails/transfer_reject-body-html.jinja | 27 +
.../emails/transfer_reject-body-text.jinja | 20 +
.../emails/transfer_reject-subject.jinja | 3 +
.../emails/transfer_request-body-html.jinja | 17 +
.../emails/transfer_request-body-text.jinja | 15 +
.../emails/transfer_request-subject.jinja | 3 +
.../emails/transfer_start-body-html.jinja | 25 +
.../emails/transfer_start-body-text.jinja | 19 +
.../emails/transfer_start-subject.jinja | 3 +
tests/integration/test_projects.py | 529 ++++++++++++++++++
25 files changed, 955 insertions(+), 7 deletions(-)
create mode 100644 taiga/projects/migrations/0036_project_transfer_token.py
create mode 100644 taiga/projects/services/transfer.py
create mode 100644 taiga/projects/templates/emails/transfer_accept-body-html.jinja
create mode 100644 taiga/projects/templates/emails/transfer_accept-body-text.jinja
create mode 100644 taiga/projects/templates/emails/transfer_accept-subject.jinja
create mode 100644 taiga/projects/templates/emails/transfer_reject-body-html.jinja
create mode 100644 taiga/projects/templates/emails/transfer_reject-body-text.jinja
create mode 100644 taiga/projects/templates/emails/transfer_reject-subject.jinja
create mode 100644 taiga/projects/templates/emails/transfer_request-body-html.jinja
create mode 100644 taiga/projects/templates/emails/transfer_request-body-text.jinja
create mode 100644 taiga/projects/templates/emails/transfer_request-subject.jinja
create mode 100644 taiga/projects/templates/emails/transfer_start-body-html.jinja
create mode 100644 taiga/projects/templates/emails/transfer_start-body-text.jinja
create mode 100644 taiga/projects/templates/emails/transfer_start-subject.jinja
diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py
index c0f1a490..76d9bc2f 100644
--- a/taiga/base/management/commands/test_emails.py
+++ b/taiga/base/management/commands/test_emails.py
@@ -187,3 +187,38 @@ class Command(BaseCommand):
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
email = cls()
email.send(test_email, context)
+
+
+ # Transfer Emails
+ context = {
+ "project": Project.objects.all().order_by("?").first(),
+ "requester": User.objects.all().order_by("?").first(),
+ }
+ email = mail_builder.transfer_request(test_email, context)
+ email.send()
+
+ context = {
+ "project": Project.objects.all().order_by("?").first(),
+ "receiver": User.objects.all().order_by("?").first(),
+ "token": "test-token",
+ "reason": "Test reason"
+ }
+ email = mail_builder.transfer_start(test_email, context)
+ email.send()
+
+ context = {
+ "project": Project.objects.all().order_by("?").first(),
+ "old_owner": User.objects.all().order_by("?").first(),
+ "new_owner": User.objects.all().order_by("?").first(),
+ "reason": "Test reason"
+ }
+ email = mail_builder.transfer_accept(test_email, context)
+ email.send()
+
+ context = {
+ "project": Project.objects.all().order_by("?").first(),
+ "rejecter": User.objects.all().order_by("?").first(),
+ "reason": "Test reason"
+ }
+ email = mail_builder.transfer_reject(test_email, context)
+ email.send()
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index af472e3d..bbb812f7 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -219,7 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
try:
dump = json.load(reader(dump))
- is_private = dump["is_private"]
+ is_private = dump.get("is_private", False)
except Exception:
raise exc.WrongArguments(_("Invalid dump format"))
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index adbaa6fe..6020217f 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -94,7 +94,7 @@ def dict_to_project(data, owner=None):
members = len(data.get("memberships", []))
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
owner,
- project=Project(is_private=data["is_private"], id=None),
+ project=Project(is_private=data.get("is_private", False), id=None),
members=members
)
if not enough_slots:
diff --git a/taiga/front/urls.py b/taiga/front/urls.py
index 76709327..499328f9 100644
--- a/taiga/front/urls.py
+++ b/taiga/front/urls.py
@@ -46,6 +46,8 @@ urls = {
"team": "/project/{0}/team/", # project.slug
+ "project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token
+
"project-admin": "/project/{0}/admin/project-profile/details", # project.slug
}
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index 2242c3ee..5a40ff7e 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -38,10 +38,6 @@ def _get_object_project(obj):
def is_project_owner(user, obj):
- """
- The owner attribute of a project is just an historical reference
- """
-
if user.is_superuser:
return True
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index a5e2838c..b1e7943c 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -328,6 +328,74 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors))
+ @detail_route(methods=["POST"])
+ def transfer_request(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "transfer_request", project)
+ services.request_project_transfer(project, request.user)
+ return response.Ok()
+
+ @detail_route(methods=['post'])
+ def transfer_start(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "transfer_start", project)
+
+ user_id = request.DATA.get('user', None)
+ if user_id is None:
+ raise exc.WrongArguments(_("Invalid user id"))
+
+ user_model = apps.get_model("users", "User")
+ try:
+ user = user_model.objects.get(id=user_id)
+
+ except user_model.DoesNotExist:
+ return response.BadRequest(_("The user doesn't exist"))
+
+ # Check the user is an admin membership from the project
+ try:
+ project.memberships.get(is_owner=True, user=user)
+ except apps.get_model("projects", "Membership").DoesNotExist:
+ return response.BadRequest(_("The user must be an admin member of the project"))
+
+ reason = request.DATA.get('reason', None)
+ transfer_token = services.start_project_transfer(project, user, reason)
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def transfer_accept(self, request, pk=None):
+ token = request.DATA.get('token', None)
+ if token is None:
+ raise exc.WrongArguments(_("Invalid token"))
+
+ project = self.get_object()
+ self.check_permissions(request, "transfer_accept", project)
+
+ members = project.memberships.count()
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ request.user,
+ project=project,
+ members=members
+ )
+ if not enough_slots:
+ raise exc.BadRequest(not_enough_slots_error)
+
+ reason = request.DATA.get('reason', None)
+ services.accept_project_transfer(project, request.user, token, reason)
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def transfer_reject(self, request, pk=None):
+ token = request.DATA.get('token', None)
+ if token is None:
+ raise exc.WrongArguments(_("Invalid token"))
+
+ project = self.get_object()
+ self.check_permissions(request, "transfer_reject", project)
+
+ reason = request.DATA.get('reason', None)
+ services.reject_project_transfer(project, request.user, token, reason)
+ return response.Ok()
+
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:
diff --git a/taiga/projects/migrations/0036_project_transfer_token.py b/taiga/projects/migrations/0036_project_transfer_token.py
new file mode 100644
index 00000000..fbbdc28b
--- /dev/null
+++ b/taiga/projects/migrations/0036_project_transfer_token.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0035_project_blocked_code'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='project',
+ name='transfer_token',
+ field=models.CharField(max_length=255, default=None, blank=True, null=True, verbose_name='project transfer token'),
+ ),
+ ]
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 60682a32..d85feb95 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -235,6 +235,9 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
verbose_name=_("tags colors"))
+ transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
+ verbose_name=_("project transfer token"))
+
#Totals:
totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("updated date time"), db_index=True)
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index ee96523f..855cc44b 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -42,6 +42,15 @@ class CanLeaveProject(PermissionComponent):
except 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')
@@ -68,6 +77,10 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
+ transfer_request_perms = IsProjectOwner()
+ transfer_start_perms = IsMainOwner()
+ transfer_reject_perms = IsProjectOwner()
+ transfer_accept_perms = IsProjectOwner()
class ProjectFansPermission(TaigaResourcePermission):
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 54d6a48b..b60c474f 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -328,7 +328,8 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
model = models.Project
read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
- "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
+ "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
+ "transfer_token")
def get_my_permissions(self, obj):
if "request" in self.context:
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index b6a51e3e..c50e8386 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -46,3 +46,7 @@ from .stats import get_stats_for_project
from .stats import get_member_stats_for_project
from .tags_colors import update_project_tags_colors_handler
+from .modules_config import get_modules_config
+
+from .transfer import request_project_transfer, start_project_transfer
+from .transfer import accept_project_transfer, reject_project_transfer
diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py
new file mode 100644
index 00000000..4ee456e2
--- /dev/null
+++ b/taiga/projects/services/transfer.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.core import signing
+from django.utils.translation import ugettext as _
+
+import datetime
+
+from taiga.base.mails import mail_builder
+from taiga.base import exceptions as exc
+
+
+def request_project_transfer(project, user):
+ template = mail_builder.transfer_request
+ email = template(project.owner, {"project": project, "requester": user})
+ email.send()
+
+
+def start_project_transfer(project, user, reason):
+ """Generates the transfer token for a project transfer and notify to the destination user
+
+ :param project: Project trying to transfer
+ :param user: Destination user
+ :param reason: Reason to transfer the project
+ """
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user.id)
+ project.transfer_token = token
+ project.save()
+
+ template = mail_builder.transfer_start
+ context = {
+ "project": project,
+ "receiver": user,
+ "token": token,
+ "reason": reason
+ }
+ email = template(project.owner, context)
+ email.send()
+
+
+def _validate_token(token, project_token, user_id):
+ signer = signing.TimestampSigner()
+
+ if project_token != token:
+ raise exc.WrongArguments(_("Token is invalid"))
+
+ try:
+ value = signer.unsign(token, max_age=datetime.timedelta(days=7))
+ except signing.SignatureExpired:
+ raise exc.WrongArguments(_("Token has expired"))
+ except signing.BadSignature:
+ raise exc.WrongArguments(_("Token is invalid"))
+
+ if str(value) != str(user_id):
+ raise exc.WrongArguments(_("Token is invalid"))
+
+
+def reject_project_transfer(project, user, token, reason):
+ _validate_token(token, project.transfer_token, user.id)
+
+ project.transfer_token = None
+ project.save()
+
+ template = mail_builder.transfer_reject
+ context = {
+ "project": project,
+ "rejecter": user,
+ "reason": reason
+ }
+ email = template(project.owner, context)
+ email.send()
+
+
+def accept_project_transfer(project, user, token, reason):
+ _validate_token(token, project.transfer_token, user.id)
+
+ old_owner = project.owner
+
+ project.transfer_token = None
+ project.owner = user
+ project.save()
+
+ template = mail_builder.transfer_accept
+ context = {
+ "project": project,
+ "old_owner": old_owner,
+ "new_owner": user,
+ "reason": reason
+ }
+ email = template(old_owner, context)
+ email.send()
diff --git a/taiga/projects/templates/emails/transfer_accept-body-html.jinja b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
new file mode 100644
index 00000000..f92233ae
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
@@ -0,0 +1,19 @@
+{% extends "emails/hero-body-html.jinja" %}
+
+{% block body %}
+ {% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
+ Hi {{old_owner_name}},
+ {{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
+ {% endtrans %}
+
+ {% if reason %}
+ {% trans %}This is the reason/comment:
{% endtrans %}
+ {{reason}}
+ {% endif %}
+
+ {% trans %}
+ From now on, your new status for this project will be "admin".
+ {% endtrans %}
+
+ {% trans %}The Taiga Team{% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_accept-body-text.jinja b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
new file mode 100644
index 00000000..0d8779d8
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
@@ -0,0 +1,17 @@
+{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
+Hi {{old_owner_name}},
+{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
+{% endtrans %}
+
+{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
+{{reason}}
+{% endif %}
+
+{% trans %}
+From now on, your new status for this project will be "admin".
+{% endtrans %}
+
+---
+{% trans %}
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_accept-subject.jinja b/taiga/projects/templates/emails/transfer_accept-subject.jinja
new file mode 100644
index 00000000..6b7c84d5
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_accept-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name %}
+[{{project}}] Project ownership transfer offer accepted!
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-html.jinja b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
new file mode 100644
index 00000000..b188b927
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
@@ -0,0 +1,27 @@
+{% extends "emails/hero-body-html.jinja" %}
+
+{% block body %}
+ {% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
+ Hi {{owner_name}},
+ {{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}".
+ {% endtrans %}
+
+ {% if reason %}
+ {% trans %}
+ This is the reason/comment:
+ {% endtrans %}
+
+ {{ reason }}
+ {% endif %}
+
+ {% trans %}
+ If you want, you can still try to transfer the project ownership to a different person.
+ {% endtrans %}
+
+
+ {% trans %}Request transfer to a different person{% endtrans %}
+
+
+ {% trans %}The Taiga Team{% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-text.jinja b/taiga/projects/templates/emails/transfer_reject-body-text.jinja
new file mode 100644
index 00000000..6acc033a
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_reject-body-text.jinja
@@ -0,0 +1,20 @@
+{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
+Hi {{owner_name}},
+{{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}".
+{% endtrans %}
+
+{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
+{{ reason }}
+{% endif %}
+
+{% trans %}
+If you want, you can still try to transfer the project ownership to a different person.
+{% endtrans %}
+
+{% trans %}Request transfer to a different person:{% endtrans %}
+{{ resolve_front_url("project-admin", project.slug) }}
+
+---
+{% trans %}
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_reject-subject.jinja b/taiga/projects/templates/emails/transfer_reject-subject.jinja
new file mode 100644
index 00000000..e6eaa127
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_reject-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name %}
+[{{project}}] Project ownership transfer declined
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_request-body-html.jinja b/taiga/projects/templates/emails/transfer_request-body-html.jinja
new file mode 100644
index 00000000..c60deb2b
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_request-body-html.jinja
@@ -0,0 +1,17 @@
+{% extends "emails/hero-body-html.jinja" %}
+
+{% block body %}
+ {% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
+ Hi {{owner_name}},
+ {{ requester_name }} has requested to become the project owner for "{{project_name}}".
+ {% endtrans %}
+
+ {% trans %}
+ Please, click on "Continue" if you would like to start the project transfer from the administration panel.
+ {% endtrans %}
+
+ {% trans %}Continue{% endtrans %}
+
+ {% trans %}The Taiga Team{% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_request-body-text.jinja b/taiga/projects/templates/emails/transfer_request-body-text.jinja
new file mode 100644
index 00000000..9b156a4d
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_request-body-text.jinja
@@ -0,0 +1,15 @@
+{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
+Hi {{owner_name}},
+{{ requester_name }} has requested to become the project owner for "{{project_name}}".
+{% endtrans %}
+
+{% trans %}
+Please, go to your project settings if you would like to start the project transfer from the administration panel.
+{% endtrans %}
+
+{{ _("Go to your project settings:") }} {{ resolve_front_url("project-admin", project.slug) }}
+
+---
+{% trans %}
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_request-subject.jinja b/taiga/projects/templates/emails/transfer_request-subject.jinja
new file mode 100644
index 00000000..1f6ff81c
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_request-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name %}
+[{{project}}] Project ownership transfer request
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_start-body-html.jinja b/taiga/projects/templates/emails/transfer_start-body-html.jinja
new file mode 100644
index 00000000..4a3a7c8a
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_start-body-html.jinja
@@ -0,0 +1,25 @@
+{% extends "emails/hero-body-html.jinja" %}
+
+{% block body %}
+ {% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %}
+ Hi {{receiver_name}},
+ {{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.
+ {% endtrans %}
+
+ {% if reason %}
+ {% trans %}
+ This is the reason/comment:
+ {% endtrans %}
+
+ {{ reason }}
+ {% endif %}
+
+ {% trans %}
+ Please, click on "Continue" to either accept or reject this proposal.
+ {% endtrans %}
+
+ {{ _("Continue") }}
+
+ {{ _("The Taiga Team") }}
+{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_start-body-text.jinja b/taiga/projects/templates/emails/transfer_start-body-text.jinja
new file mode 100644
index 00000000..8403c790
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_start-body-text.jinja
@@ -0,0 +1,19 @@
+{% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %}
+Hi {{receiver_name}},
+{{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.
+{% endtrans %}
+
+{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
+{{ reason }}
+{% endif %}
+
+{% trans %}
+Please, go to the following link to either accept or reject this proposal.
+{% endtrans %}
+
+{{ _("Accept or reject the project transfer:") }} {{ resolve_front_url("project-transfer", project.slug, project.transfer_token) }}
+
+---
+{% trans %}
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/templates/emails/transfer_start-subject.jinja b/taiga/projects/templates/emails/transfer_start-subject.jinja
new file mode 100644
index 00000000..d0e34d3a
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_start-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name %}
+[{{project}}] Project ownership transfer offer
+{% endtrans %}
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 1627d537..0bbae37a 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1,6 +1,8 @@
from django.core.urlresolvers import reverse
from django.conf import settings
from django.core.files import File
+from django.core import mail
+from django.core import signing
from taiga.base.utils import json
from taiga.projects.services import stats as stats_services
@@ -19,6 +21,17 @@ import pytest
pytestmark = pytest.mark.django_db
+class ExpiredSigner(signing.TimestampSigner):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.salt = "django.core.signing.TimestampSigner"
+
+ def timestamp(self):
+ from django.utils import baseconv
+ import time
+ time_in_the_far_past = int(time.time()) - 24*60*60*1000
+ return baseconv.base62.encode(time_in_the_far_past)
+
def test_get_project_by_slug(client):
project = f.create_project()
@@ -698,3 +711,519 @@ def test_project_list_with_search_query_order_by_ranking(client):
assert response.data[0]["id"] == project3.id
assert response.data[1]["id"] == project2.id
assert response.data[2]["id"] == project1.id
+
+
+def test_transfer_request_from_not_anonimous(client):
+ user = f.UserFactory.create()
+ project = f.create_project(anon_permissions=["view_project"])
+
+ url = reverse("projects-transfer-request", args=(project.id,))
+
+ mail.outbox = []
+
+ response = client.json.post(url)
+ assert response.status_code == 401
+ assert len(mail.outbox) == 0
+
+
+def test_transfer_request_from_not_project_member(client):
+ user = f.UserFactory.create()
+ project = f.create_project(public_permissions=["view_project"])
+
+ url = reverse("projects-transfer-request", args=(project.id,))
+
+ mail.outbox = []
+
+ client.login(user)
+ response = client.json.post(url)
+ assert response.status_code == 403
+ assert len(mail.outbox) == 0
+
+
+def test_transfer_request_from_not_admin_member(client):
+ user = f.UserFactory.create()
+ project = f.create_project()
+ role = f.RoleFactory(project=project, permissions=["view_project"])
+ f.MembershipFactory(user=user, project=project, role=role, is_owner=False)
+
+ url = reverse("projects-transfer-request", args=(project.id,))
+
+ mail.outbox = []
+
+ client.login(user)
+ response = client.json.post(url)
+ assert response.status_code == 403
+ assert len(mail.outbox) == 0
+
+
+def test_transfer_request_from_admin_member(client):
+ user = f.UserFactory.create()
+ project = f.create_project()
+ role = f.RoleFactory(project=project, permissions=["view_project"])
+ f.MembershipFactory(user=user, project=project, role=role, is_owner=True)
+
+ url = reverse("projects-transfer-request", args=(project.id,))
+
+ mail.outbox = []
+
+ client.login(user)
+ response = client.json.post(url)
+ assert response.status_code == 200
+ assert len(mail.outbox) == 1
+
+def test_project_transfer_start_to_not_a_membership(client):
+ user_from = f.UserFactory.create()
+ project = f.create_project(owner=user_from)
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+
+ client.login(user_from)
+ url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
+
+ data = {
+ "user": 666,
+ }
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert "The user doesn't exist" in response.data
+
+
+def test_project_transfer_start_to_not_a_membership_admin(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ project = f.create_project(owner=user_from)
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project)
+
+ client.login(user_from)
+ url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
+
+ data = {
+ "user": user_to.id,
+ }
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert "The user must be" in response.data
+
+
+def test_project_transfer_start_to_a_valid_user(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ project = f.create_project(owner=user_from)
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_from)
+ url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
+
+ data = {
+ "user": user_to.id,
+ }
+ mail.outbox = []
+
+ assert project.transfer_token is None
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.id)
+ assert project.transfer_token is not None
+ assert len(mail.outbox) == 1
+
+
+def test_project_transfer_reject_from_admin_member_without_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {}
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_reject_from_not_admin_member(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=False)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 403
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_reject_from_admin_member_with_invalid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ project = f.create_project(owner=user_from, transfer_token="invalid-token")
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": "invalid-token",
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_reject_from_admin_member_with_other_user_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ other_user = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(other_user.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_reject_from_admin_member_with_expired_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = ExpiredSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token has expired" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_reject_from_admin_member_with_valid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+ assert len(mail.outbox) == 1
+ assert mail.outbox[0].to == [user_from.email]
+
+
+def test_project_transfer_accept_from_admin_member_without_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {}
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_accept_from_not_admin_member(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=False)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 403
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_accept_from_admin_member_with_invalid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ project = f.create_project(owner=user_from, transfer_token="invalid-token")
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": "invalid-token",
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_accept_from_admin_member_with_other_user_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ other_user = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(other_user.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_accept_from_admin_member_with_expired_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = ExpiredSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token has expired" == response.data["_error_message"]
+ assert len(mail.outbox) == 0
+
+
+def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_slots(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_private_projects=0)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert len(mail.outbox) == 0
+ project = Project.objects.get(pk=project.pk)
+ assert project.owner.id == user_from.id
+ assert project.transfer_token is not None
+
+
+def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_public_project_slots(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_members_public_projects=5)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=False)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert len(mail.outbox) == 0
+ project = Project.objects.get(pk=project.pk)
+ assert project.owner.id == user_from.id
+ assert project.transfer_token is not None
+
+
+def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_private_project_slots(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_members_private_projects=5)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert len(mail.outbox) == 0
+ project = Project.objects.get(pk=project.pk)
+ assert project.owner.id == user_from.id
+ assert project.transfer_token is not None
+
+
+def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_slots(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_private_projects=1)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_to, project=project, is_owner=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+ assert len(mail.outbox) == 1
+ assert mail.outbox[0].to == [user_from.email]
+ project = Project.objects.get(pk=project.pk)
+ assert project.owner.id == user_to.id
+ assert project.transfer_token is None
From f8f0967b8ec6554d7a79fdb0bb9d36913f840a45 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 5 Feb 2016 11:32:44 +0100
Subject: [PATCH 020/105] Updating CHANGELOG: including transfer projects
feature
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 961dad9a..68a998dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### Features
- Blocked projects support
+- Transfer projects ownership support
### Misc
- Lots of small and not so small bugfixes.
From 14bdf474423da1878df9fd87754d4327e854dfa0 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 5 Feb 2016 11:35:11 +0100
Subject: [PATCH 021/105] Updating CHANGELOG: including max projects and
memberships feature
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68a998dc..b0b36148 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
### Features
- Blocked projects support
- Transfer projects ownership support
+- Customizable max private and public projects per user
+- Customizable max of memberships per owned private and public projects
### Misc
- Lots of small and not so small bugfixes.
From 172ce73007d4b7fee01f49e0f7e7d52dac204b89 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 5 Feb 2016 08:11:22 +0100
Subject: [PATCH 022/105] Improving CSV reports performance
---
taiga/projects/issues/services.py | 24 +++++++++++---
taiga/projects/tasks/services.py | 24 +++++++++++---
taiga/projects/userstories/services.py | 44 +++++++++++++++++++-------
3 files changed, 70 insertions(+), 22 deletions(-)
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 29452c8b..6ae35b2a 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -28,7 +28,9 @@ from taiga.base.utils import db, text
from taiga.projects.issues.apps import (
connect_issues_signals,
disconnect_issues_signals)
-from taiga.projects.votes import services as votes_services
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+
from . import models
@@ -88,9 +90,21 @@ def issues_to_csv(project, queryset):
"attachments", "external_reference", "tags",
"watchers", "voters",
"created_date", "modified_date", "finished_date"]
- for custom_attr in project.issuecustomattributes.all():
+
+ custom_attrs = project.issuecustomattributes.all()
+ for custom_attr in custom_attrs:
fieldnames.append(custom_attr.name)
+ queryset = queryset.prefetch_related("attachments",
+ "generated_user_stories",
+ "custom_attributes_values")
+ queryset = queryset.select_related("owner",
+ "assigned_to",
+ "status",
+ "project")
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+
writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
writer.writeheader()
for issue in queryset:
@@ -111,14 +125,14 @@ def issues_to_csv(project, queryset):
"attachments": issue.attachments.count(),
"external_reference": issue.external_reference,
"tags": ",".join(issue.tags or []),
- "watchers": [u.id for u in issue.get_watchers()],
- "voters": votes_services.get_voters(issue).count(),
+ "watchers": issue.watchers,
+ "voters": issue.total_voters,
"created_date": issue.created_date,
"modified_date": issue.modified_date,
"finished_date": issue.finished_date,
}
- for custom_attr in project.issuecustomattributes.all():
+ for custom_attr in custom_attrs:
value = issue.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
issue_data[custom_attr.name] = value
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index d40394a2..7fad684d 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -24,7 +24,8 @@ from taiga.projects.tasks.apps import (
connect_tasks_signals,
disconnect_tasks_signals)
from taiga.events import events
-from taiga.projects.votes import services as votes_services
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models
@@ -99,9 +100,22 @@ def tasks_to_csv(project, queryset):
"status", "is_iocaine", "is_closed", "us_order",
"taskboard_order", "attachments", "external_reference", "tags",
"watchers", "voters"]
- for custom_attr in project.taskcustomattributes.all():
+
+ custom_attrs = project.taskcustomattributes.all()
+ for custom_attr in custom_attrs:
fieldnames.append(custom_attr.name)
+ queryset = queryset.prefetch_related("attachments",
+ "custom_attributes_values")
+ queryset = queryset.select_related("milestone",
+ "owner",
+ "assigned_to",
+ "status",
+ "project")
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+
writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
writer.writeheader()
for task in queryset:
@@ -123,10 +137,10 @@ def tasks_to_csv(project, queryset):
"attachments": task.attachments.count(),
"external_reference": task.external_reference,
"tags": ",".join(task.tags or []),
- "watchers": [u.id for u in task.get_watchers()],
- "voters": votes_services.get_voters(task).count(),
+ "watchers": task.watchers,
+ "voters": task.total_voters,
}
- for custom_attr in project.taskcustomattributes.all():
+ for custom_attr in custom_attrs:
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
task_data[custom_attr.name] = value
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index edf98b64..f7685481 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -32,7 +32,8 @@ from taiga.projects.userstories.apps import (
disconnect_userstories_signals)
from taiga.events import events
-from taiga.projects.votes import services as votes_services
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models
@@ -132,8 +133,11 @@ def userstories_to_csv(project,queryset):
fieldnames = ["ref", "subject", "description", "milestone", "owner",
"owner_full_name", "assigned_to", "assigned_to_full_name",
"status", "is_closed"]
- for role in project.roles.filter(computable=True).order_by('name'):
+
+ roles = project.roles.filter(computable=True).order_by('slug')
+ for role in roles:
fieldnames.append("{}-points".format(role.slug))
+
fieldnames.append("total-points")
fieldnames += ["backlog_order", "sprint_order", "kanban_order",
@@ -143,9 +147,26 @@ def userstories_to_csv(project,queryset):
"tags",
"watchers", "voters"]
- for custom_attr in project.userstorycustomattributes.all():
+ custom_attrs = project.userstorycustomattributes.all()
+ for custom_attr in custom_attrs:
fieldnames.append(custom_attr.name)
+ queryset = queryset.prefetch_related("role_points",
+ "role_points__points",
+ "role_points__role",
+ "tasks",
+ "attachments",
+ "custom_attributes_values")
+ queryset = queryset.select_related("milestone",
+ "project",
+ "status",
+ "owner",
+ "assigned_to",
+ "generated_from_issue")
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+
writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
writer.writeheader()
for us in queryset:
@@ -173,18 +194,17 @@ def userstories_to_csv(project,queryset):
"external_reference": us.external_reference,
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []),
- "watchers": [u.id for u in us.get_watchers()],
- "voters": votes_services.get_voters(us).count(),
+ "watchers": us.watchers,
+ "voters": us.total_voters,
}
- for role in us.project.roles.filter(computable=True).order_by('name'):
- if us.role_points.filter(role_id=role.id).count() == 1:
- row["{}-points".format(role.slug)] = us.role_points.get(role_id=role.id).points.value
- else:
- row["{}-points".format(role.slug)] = 0
+ us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()}
+ for role in roles:
+ row["{}-points".format(role.slug)] = us_role_points_by_role_id.get(role.id, 0)
+
row['total-points'] = us.get_total_points()
- for custom_attr in project.userstorycustomattributes.all():
+ for custom_attr in custom_attrs:
value = us.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
row[custom_attr.name] = value
@@ -274,7 +294,7 @@ def _get_userstories_assigned_to(project, queryset):
"full_name": full_name or "",
"count": count,
})
-
+
if id is None:
none_valued_added = True
From 6cf3c5727e367ae300b6525086b08069accb7afb Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 4 Feb 2016 20:23:03 +0100
Subject: [PATCH 023/105] Fixing serializers validation for duplicated names in
priorities, severities and issue types
---
taiga/projects/milestones/serializers.py | 21 +-----
taiga/projects/mixins/serializers.py | 40 +++++++++++
taiga/projects/serializers.py | 85 +++---------------------
tests/unit/test_serializer_mixins.py | 44 ++++++++++++
4 files changed, 94 insertions(+), 96 deletions(-)
create mode 100644 taiga/projects/mixins/serializers.py
create mode 100644 tests/unit/test_serializer_mixins.py
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index 2e6d95ac..191b2234 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -21,12 +21,12 @@ from taiga.base.api import serializers
from taiga.base.utils import json
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
-
+from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from ..userstories.serializers import UserStoryListSerializer
from . import models
-class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
+class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ValidateDuplicatedNameInProjectMixin):
user_stories = UserStoryListSerializer(many=True, required=False, read_only=True)
total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points")
@@ -40,20 +40,3 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ser
def get_closed_points(self, obj):
return sum(obj.closed_points.values())
-
- def validate_name(self, attrs, source):
- """
- Check the milestone name is not duplicated in the project on creation
- """
- qs = None
- # If the milestone exists:
- if self.object and attrs.get("name", None):
- qs = models.Milestone.objects.filter(project=self.object.project, name=attrs[source]).exclude(pk=self.object.pk)
-
- if not self.object and attrs.get("project", None) and attrs.get("name", None):
- qs = models.Milestone.objects.filter(project=attrs["project"], name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py
new file mode 100644
index 00000000..edaa0c1a
--- /dev/null
+++ b/taiga/projects/mixins/serializers.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import serializers
+
+from django.utils.translation import ugettext as _
+
+class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
+
+ def validate_name(self, attrs, source):
+ """
+ Check the points name is not duplicated in the project on creation
+ """
+ model = self.opts.model
+ qs = None
+ # If the object exists:
+ if self.object and attrs.get(source, None):
+ qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id)
+
+ if not self.object and attrs.get("project", None) and attrs.get(source, None):
+ qs = model.objects.filter(project=attrs["project"], name=attrs[source])
+
+ if qs and qs.exists():
+ raise serializers.ValidationError(_("Name duplicated for the project"))
+
+ return attrs
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index b60c474f..784a5a8a 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -34,6 +34,7 @@ from taiga.users.validators import RoleExistsValidator
from taiga.permissions.service import get_user_project_permissions
from taiga.permissions.service import is_project_owner
+from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from . import models
from . import services
@@ -49,53 +50,17 @@ from .likes.mixins.serializers import FanResourceSerializerMixin
## Custom values for selectors
######################################################
-class PointsSerializer(serializers.ModelSerializer):
+class PointsSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.Points
i18n_fields = ("name",)
- def validate_name(self, attrs, source):
- """
- Check the points name is not duplicated in the project on creation
- """
- qs = None
- # If the user story status exists:
- if self.object and attrs.get("name", None):
- qs = models.Points.objects.filter(project=self.object.project, name=attrs[source])
- if not self.object and attrs.get("project", None) and attrs.get("name", None):
- qs = models.Points.objects.filter(project=attrs["project"], name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
-
-class UserStoryStatusSerializer(serializers.ModelSerializer):
+class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.UserStoryStatus
i18n_fields = ("name",)
- def validate_name(self, attrs, source):
- """
- Check the status name is not duplicated in the project on creation
- """
- qs = None
- # If the user story status exists:
- if self.object and attrs.get("name", None):
- qs = models.UserStoryStatus.objects.filter(project=self.object.project,
- name=attrs[source])
-
- if not self.object and attrs.get("project", None) and attrs.get("name", None):
- qs = models.UserStoryStatus.objects.filter(project=attrs["project"],
- name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
class Meta:
@@ -104,28 +69,11 @@ class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
fields = ("name", "color")
-class TaskStatusSerializer(serializers.ModelSerializer):
+class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.TaskStatus
i18n_fields = ("name",)
- def validate_name(self, attrs, source):
- """
- Check the task name is not duplicated in the project on creation
- """
- qs = None
- # If the user story status exists:
- if self.object and attrs.get("name", None):
- qs = models.TaskStatus.objects.filter(project=self.object.project, name=attrs[source])
-
- if not self.object and attrs.get("project", None) and attrs.get("name", None):
- qs = models.TaskStatus.objects.filter(project=attrs["project"], name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
@@ -135,40 +83,23 @@ class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
fields = ("name", "color")
-class SeveritySerializer(serializers.ModelSerializer):
+class SeveritySerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.Severity
i18n_fields = ("name",)
-class PrioritySerializer(serializers.ModelSerializer):
+class PrioritySerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.Priority
i18n_fields = ("name",)
-class IssueStatusSerializer(serializers.ModelSerializer):
+class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.IssueStatus
i18n_fields = ("name",)
- def validate_name(self, attrs, source):
- """
- Check the issue name is not duplicated in the project on creation
- """
- qs = None
- # If the user story status exists:
- if self.object and attrs.get("name", None):
- qs = models.IssueStatus.objects.filter(project=self.object.project, name=attrs[source])
-
- if not self.object and attrs.get("project", None) and attrs.get("name", None):
- qs = models.IssueStatus.objects.filter(project=attrs["project"], name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
class BasicIssueStatusSerializer(serializers.ModelSerializer):
class Meta:
@@ -177,7 +108,7 @@ class BasicIssueStatusSerializer(serializers.ModelSerializer):
fields = ("name", "color")
-class IssueTypeSerializer(serializers.ModelSerializer):
+class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.IssueType
i18n_fields = ("name",)
diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py
new file mode 100644
index 00000000..e9f94e0c
--- /dev/null
+++ b/tests/unit/test_serializer_mixins.py
@@ -0,0 +1,44 @@
+import pytest
+
+from .. import factories as f
+from django.db import models
+from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
+from taiga.projects.models import Project
+
+pytestmark = pytest.mark.django_db(transaction=True)
+
+import factory
+
+
+class TestingProjectModel(models.Model):
+ pass
+
+class TestingModelWithNameAttribute(models.Model):
+ name = models.CharField(max_length=255, null=False, blank=False)
+ project = models.ForeignKey(TestingProjectModel, null=False, blank=False)
+
+
+class TestingSerializer(ValidateDuplicatedNameInProjectMixin):
+ class Meta:
+ model = TestingModelWithNameAttribute
+
+
+def test_duplicated_name_validation():
+ project = TestingProjectModel.objects.create()
+ instance_1 = TestingModelWithNameAttribute.objects.create(name="1", project=project)
+ instance_2 = TestingModelWithNameAttribute.objects.create(name="2", project=project)
+
+ # No duplicated_name
+ serializer = TestingSerializer(data={"name": "3", "project": project.id})
+
+ assert serializer.is_valid()
+
+ # Create duplicated_name
+ serializer = TestingSerializer(data={"name": "1", "project": project.id})
+
+ assert not serializer.is_valid()
+
+ # Update name to existing one
+ serializer = TestingSerializer(data={"id": instance_2.id, "name": "1","project": project.id})
+
+ assert not serializer.is_valid()
From e8bd1f8e6b23de48c09d5ea63e028ec6867d5927 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 8 Feb 2016 14:12:11 +0100
Subject: [PATCH 024/105] Fixing project delete precondition
---
taiga/projects/api.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index b1e7943c..50fe5f46 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -430,10 +430,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
if obj is None:
raise Http404
- obj.delete_related_content()
-
self.pre_delete(obj)
self.pre_conditions_on_delete(obj)
+ obj.delete_related_content()
obj.delete()
self.post_delete(obj)
return response.NoContent()
From 7ec0ecfc334bc657be743dc29fd0c1c378aab488 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 8 Feb 2016 14:12:58 +0100
Subject: [PATCH 025/105] Fixing blocked by owner leave error message
---
taiga/projects/choices.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py
index 5a7d8765..9bebce00 100644
--- a/taiga/projects/choices.py
+++ b/taiga/projects/choices.py
@@ -29,5 +29,5 @@ BLOCKED_BY_STAFF = "blocked-by-staff"
BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving"
BLOCKING_CODES = [
(BLOCKED_BY_STAFF, _("This project was blocked by staff")),
- (BLOCKED_BY_OWNER_LEAVING, _("This project was because the owner left"))
+ (BLOCKED_BY_OWNER_LEAVING, _("This project was blocked because the owner left"))
]
From 55a1a261f400d13234f96b877ef1fec26a73f27d Mon Sep 17 00:00:00 2001
From: Andrea Stagi
Date: Tue, 26 Jan 2016 20:36:06 +0100
Subject: [PATCH 026/105] Add new custom attribute type: URL
---
CHANGELOG.md | 1 +
taiga/projects/custom_attributes/choices.py | 4 +++-
taiga/projects/management/commands/sample_data.py | 10 +++++++++-
3 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0b36148..5788587c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
## 2.0.0 ??? (unreleased)
### Features
+- Ability to create url custom fields. (thanks to [@astagi](https://github.com/astagi)).
- Blocked projects support
- Transfer projects ownership support
- Customizable max private and public projects per user
diff --git a/taiga/projects/custom_attributes/choices.py b/taiga/projects/custom_attributes/choices.py
index dd6c15e9..fadcc788 100644
--- a/taiga/projects/custom_attributes/choices.py
+++ b/taiga/projects/custom_attributes/choices.py
@@ -21,9 +21,11 @@ from django.utils.translation import ugettext_lazy as _
TEXT_TYPE = "text"
MULTILINE_TYPE = "multiline"
DATE_TYPE = "date"
+URL_TYPE = "url"
TYPES_CHOICES = (
(TEXT_TYPE, _("Text")),
(MULTILINE_TYPE, _("Multi-Line Text")),
- (DATE_TYPE, _("Date"))
+ (DATE_TYPE, _("Date")),
+ (URL_TYPE, _("Url"))
)
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 4af2c699..02d6ad54 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -39,7 +39,7 @@ from taiga.projects.issues.models import *
from taiga.projects.wiki.models import *
from taiga.projects.attachments.models import *
from taiga.projects.custom_attributes.models import *
-from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE
+from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE, URL_TYPE
from taiga.projects.history.services import take_snapshot
from taiga.projects.likes.services import add_like
from taiga.projects.votes.services import add_vote
@@ -92,6 +92,12 @@ SUBJECT_CHOICES = [
"Support for bulk actions",
"Migrate to Python 3 and milk a beautiful cow"]
+URL_CHOICES = [
+ "https://taiga.io",
+ "https://blog.taiga.io",
+ "https://tree.taiga.io",
+ "https://tribe.taiga.io"]
+
BASE_USERS = getattr(settings, "SAMPLE_DATA_BASE_USERS", {})
NUM_USERS = getattr(settings, "SAMPLE_DATA_NUM_USERS", 10)
NUM_INVITATIONS =getattr(settings, "SAMPLE_DATA_NUM_INVITATIONS", 2)
@@ -279,6 +285,8 @@ class Command(BaseCommand):
return self.sd.paragraphs(2, 4)
if type == DATE_TYPE:
return self.sd.future_date(min_distance=0, max_distance=365)
+ if type == DATE_URL:
+ return self.sd.choice(URL_CHOICES)
return None
def create_bug(self, project):
From cb253f7b4839ccb6098d29ef43db8c3904d5d217 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 8 Feb 2016 18:53:24 +0100
Subject: [PATCH 027/105] Add some missed migrations
---
.../migrations/0007_auto_20160208_1751.py | 29 +++++++++++++++++++
.../migrations/0037_auto_20160208_1751.py | 19 ++++++++++++
.../migrations/0017_auto_20160208_1751.py | 22 ++++++++++++++
3 files changed, 70 insertions(+)
create mode 100644 taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py
create mode 100644 taiga/projects/migrations/0037_auto_20160208_1751.py
create mode 100644 taiga/users/migrations/0017_auto_20160208_1751.py
diff --git a/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py b/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py
new file mode 100644
index 00000000..08acc42f
--- /dev/null
+++ b/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('custom_attributes', '0006_auto_20151014_1645'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='issuecustomattribute',
+ name='type',
+ field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
+ ),
+ migrations.AlterField(
+ model_name='taskcustomattribute',
+ name='type',
+ field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
+ ),
+ migrations.AlterField(
+ model_name='userstorycustomattribute',
+ name='type',
+ field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0037_auto_20160208_1751.py b/taiga/projects/migrations/0037_auto_20160208_1751.py
new file mode 100644
index 00000000..f0af4359
--- /dev/null
+++ b/taiga/projects/migrations/0037_auto_20160208_1751.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0036_project_transfer_token'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='blocked_code',
+ field=models.CharField(max_length=255, blank=True, verbose_name='blocked code', choices=[('blocked-by-staff', 'This project was blocked by staff'), ('blocked-by-owner-leaving', 'This project was blocked because the owner left')], null=True, default=None),
+ ),
+ ]
diff --git a/taiga/users/migrations/0017_auto_20160208_1751.py b/taiga/users/migrations/0017_auto_20160208_1751.py
new file mode 100644
index 00000000..224248e1
--- /dev/null
+++ b/taiga/users/migrations/0017_auto_20160208_1751.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0016_auto_20160204_1050'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='role',
+ options={'ordering': ['order', 'slug'], 'verbose_name': 'role', 'verbose_name_plural': 'roles'},
+ ),
+ migrations.AlterModelOptions(
+ name='user',
+ options={'ordering': ['username'], 'verbose_name': 'user', 'verbose_name_plural': 'users'},
+ ),
+ ]
From b271d0ce47f099c4eb5837e6cd4baec0c379500f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Mon, 8 Feb 2016 14:24:13 +0100
Subject: [PATCH 028/105] Fixe transfer project emails template and text
---
.../projects/templates/emails/transfer_accept-body-html.jinja | 4 +---
.../projects/templates/emails/transfer_reject-body-html.jinja | 4 +---
.../templates/emails/transfer_request-body-html.jinja | 4 +---
.../projects/templates/emails/transfer_start-body-html.jinja | 4 +---
4 files changed, 4 insertions(+), 12 deletions(-)
diff --git a/taiga/projects/templates/emails/transfer_accept-body-html.jinja b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
index f92233ae..2606af4b 100644
--- a/taiga/projects/templates/emails/transfer_accept-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
@@ -1,4 +1,4 @@
-{% extends "emails/hero-body-html.jinja" %}
+{% extends "emails/base-body-html.jinja" %}
{% block body %}
{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
@@ -14,6 +14,4 @@
{% trans %}
From now on, your new status for this project will be "admin".
{% endtrans %}
-
- {% trans %}The Taiga Team{% endtrans %}
{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-html.jinja b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
index b188b927..ac17c5e7 100644
--- a/taiga/projects/templates/emails/transfer_reject-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
@@ -1,4 +1,4 @@
-{% extends "emails/hero-body-html.jinja" %}
+{% extends "emails/base-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
@@ -22,6 +22,4 @@
title="{% trans %}Request transfer to a different person{% endtrans %}">
{% trans %}Request transfer to a different person{% endtrans %}
-
- {% trans %}The Taiga Team{% endtrans %}
{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_request-body-html.jinja b/taiga/projects/templates/emails/transfer_request-body-html.jinja
index c60deb2b..500e3433 100644
--- a/taiga/projects/templates/emails/transfer_request-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_request-body-html.jinja
@@ -1,4 +1,4 @@
-{% extends "emails/hero-body-html.jinja" %}
+{% extends "emails/base-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
@@ -12,6 +12,4 @@
{% trans %}Continue{% endtrans %}
-
- {% trans %}The Taiga Team{% endtrans %}
{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_start-body-html.jinja b/taiga/projects/templates/emails/transfer_start-body-html.jinja
index 4a3a7c8a..390b2584 100644
--- a/taiga/projects/templates/emails/transfer_start-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_start-body-html.jinja
@@ -1,4 +1,4 @@
-{% extends "emails/hero-body-html.jinja" %}
+{% extends "emails/base-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %}
@@ -20,6 +20,4 @@
{{ _("Continue") }}
-
- {{ _("The Taiga Team") }}
{% endblock %}
From c3c06bebe5b0aef306e2611da5773c0f742592d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 8 Feb 2016 17:39:12 +0100
Subject: [PATCH 029/105] Issue #3886: Fix empty full_names in Story and Issues
filter lists
---
taiga/projects/issues/services.py | 204 +++++++++++++------------
taiga/projects/userstories/services.py | 88 ++++++-----
2 files changed, 152 insertions(+), 140 deletions(-)
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 6ae35b2a..6d1871e3 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -149,21 +149,22 @@ def _get_issues_statuses(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT status_id, count(status_id) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY status_id
- )
- SELECT "projects_issuestatus"."id",
- "projects_issuestatus"."name",
- "projects_issuestatus"."color",
- "projects_issuestatus"."order",
- COALESCE(counters.count, 0)
- FROM "projects_issuestatus"
+ SELECT status_id, count(status_id) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY status_id
+ )
+
+ SELECT "projects_issuestatus"."id",
+ "projects_issuestatus"."name",
+ "projects_issuestatus"."color",
+ "projects_issuestatus"."order",
+ COALESCE(counters.count, 0)
+ FROM "projects_issuestatus"
LEFT OUTER JOIN counters ON counters.status_id = projects_issuestatus.id
- WHERE "projects_issuestatus"."project_id" = %s
- ORDER BY "projects_issuestatus"."order";
+ WHERE "projects_issuestatus"."project_id" = %s
+ ORDER BY "projects_issuestatus"."order";
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -190,21 +191,22 @@ def _get_issues_types(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT type_id, count(type_id) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY type_id
- )
- SELECT "projects_issuetype"."id",
- "projects_issuetype"."name",
- "projects_issuetype"."color",
- "projects_issuetype"."order",
- COALESCE(counters.count, 0)
- FROM "projects_issuetype"
+ SELECT type_id, count(type_id) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY type_id
+ )
+
+ SELECT "projects_issuetype"."id",
+ "projects_issuetype"."name",
+ "projects_issuetype"."color",
+ "projects_issuetype"."order",
+ COALESCE(counters.count, 0)
+ FROM "projects_issuetype"
LEFT OUTER JOIN counters ON counters.type_id = projects_issuetype.id
- WHERE "projects_issuetype"."project_id" = %s
- ORDER BY "projects_issuetype"."order";
+ WHERE "projects_issuetype"."project_id" = %s
+ ORDER BY "projects_issuetype"."order";
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -231,21 +233,22 @@ def _get_issues_priorities(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT priority_id, count(priority_id) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY priority_id
- )
- SELECT "projects_priority"."id",
- "projects_priority"."name",
- "projects_priority"."color",
- "projects_priority"."order",
- COALESCE(counters.count, 0)
- FROM "projects_priority"
+ SELECT priority_id, count(priority_id) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY priority_id
+ )
+
+ SELECT "projects_priority"."id",
+ "projects_priority"."name",
+ "projects_priority"."color",
+ "projects_priority"."order",
+ COALESCE(counters.count, 0)
+ FROM "projects_priority"
LEFT OUTER JOIN counters ON counters.priority_id = projects_priority.id
- WHERE "projects_priority"."project_id" = %s
- ORDER BY "projects_priority"."order";
+ WHERE "projects_priority"."project_id" = %s
+ ORDER BY "projects_priority"."order";
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -272,21 +275,22 @@ def _get_issues_severities(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT severity_id, count(severity_id) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY severity_id
- )
- SELECT "projects_severity"."id",
- "projects_severity"."name",
- "projects_severity"."color",
- "projects_severity"."order",
- COALESCE(counters.count, 0)
- FROM "projects_severity"
+ SELECT severity_id, count(severity_id) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY severity_id
+ )
+
+ SELECT "projects_severity"."id",
+ "projects_severity"."name",
+ "projects_severity"."color",
+ "projects_severity"."order",
+ COALESCE(counters.count, 0)
+ FROM "projects_severity"
LEFT OUTER JOIN counters ON counters.severity_id = projects_severity.id
- WHERE "projects_severity"."project_id" = %s
- ORDER BY "projects_severity"."order";
+ WHERE "projects_severity"."project_id" = %s
+ ORDER BY "projects_severity"."order";
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -313,28 +317,30 @@ def _get_issues_assigned_to(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT assigned_to_id, count(assigned_to_id) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where} AND "issues_issue"."assigned_to_id" IS NOT NULL
- GROUP BY assigned_to_id
- )
- SELECT
- "projects_membership"."user_id" user_id,
- "users_user"."full_name",
- COALESCE("counters".count, 0) count
- FROM projects_membership
+ SELECT assigned_to_id, count(assigned_to_id) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where} AND "issues_issue"."assigned_to_id" IS NOT NULL
+ GROUP BY assigned_to_id
+ )
+
+ SELECT "projects_membership"."user_id" user_id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- unassigned issues
UNION
- SELECT NULL user_id, NULL, count(coalesce(assigned_to_id, -1)) count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where} AND "issues_issue"."assigned_to_id" IS NULL
- GROUP BY assigned_to_id
+
+ SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where} AND "issues_issue"."assigned_to_id" IS NULL
+ GROUP BY assigned_to_id
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -343,10 +349,10 @@ def _get_issues_assigned_to(project, queryset):
result = []
none_valued_added = False
- for id, full_name, count in rows:
+ for id, full_name, username, count in rows:
result.append({
"id": id,
- "full_name": full_name or "",
+ "full_name": full_name or username or "",
"count": count,
})
@@ -372,30 +378,32 @@ def _get_issues_owners(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT "issues_issue"."owner_id" owner_id, count("issues_issue"."owner_id") count
- FROM "issues_issue"
- INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY "issues_issue"."owner_id"
- )
- SELECT
- "projects_membership"."user_id" id,
- "users_user"."full_name",
- COALESCE("counters".count, 0) count
- FROM projects_membership
+ SELECT "issues_issue"."owner_id" owner_id, count("issues_issue"."owner_id") count
+ FROM "issues_issue"
+ INNER JOIN "projects_project" ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY "issues_issue"."owner_id"
+ )
+
+ SELECT "projects_membership"."user_id" id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
-- System users
UNION
- SELECT
- "users_user"."id" user_id,
- "users_user"."full_name" full_name,
- COALESCE("counters".count, 0) count
- FROM users_user
- LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
- WHERE ("users_user"."is_system" IS TRUE)
+
+ SELECT "users_user"."id" user_id,
+ "users_user"."full_name" full_name,
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM users_user
+ LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
+ WHERE ("users_user"."is_system" IS TRUE)
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -403,11 +411,11 @@ def _get_issues_owners(project, queryset):
rows = cursor.fetchall()
result = []
- for id, full_name, count in rows:
+ for id, full_name, username, count in rows:
if count > 0:
result.append({
"id": id,
- "full_name": full_name,
+ "full_name": full_name or username or "",
"count": count,
})
return sorted(result, key=itemgetter("full_name"))
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index f7685481..4270ec54 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -258,28 +258,30 @@ def _get_userstories_assigned_to(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT assigned_to_id, count(assigned_to_id) count
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NOT NULL
- GROUP BY assigned_to_id
- )
- SELECT
- "projects_membership"."user_id" user_id,
- "users_user"."full_name",
- COALESCE("counters".count, 0) count
- FROM projects_membership
+ SELECT assigned_to_id, count(assigned_to_id) count
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NOT NULL
+ GROUP BY assigned_to_id
+ )
+
+ SELECT "projects_membership"."user_id" user_id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- unassigned userstories
UNION
- SELECT NULL user_id, NULL, count(coalesce(assigned_to_id, -1)) count
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL
- GROUP BY assigned_to_id
+
+ SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL
+ GROUP BY assigned_to_id
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -288,10 +290,10 @@ def _get_userstories_assigned_to(project, queryset):
result = []
none_valued_added = False
- for id, full_name, count in rows:
+ for id, full_name, username, count in rows:
result.append({
"id": id,
- "full_name": full_name or "",
+ "full_name": full_name or username or "",
"count": count,
})
@@ -317,30 +319,32 @@ def _get_userstories_owners(project, queryset):
extra_sql = """
WITH counters AS (
- SELECT "userstories_userstory"."owner_id" owner_id, count(coalesce("userstories_userstory"."owner_id", -1)) count
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY "userstories_userstory"."owner_id"
- )
- SELECT
- "projects_membership"."user_id" id,
- "users_user"."full_name",
- COALESCE("counters".count, 0) count
- FROM projects_membership
+ SELECT "userstories_userstory"."owner_id" owner_id, count(coalesce("userstories_userstory"."owner_id", -1)) count
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY "userstories_userstory"."owner_id"
+ )
+
+ SELECT "projects_membership"."user_id" id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
-- System users
UNION
- SELECT
- "users_user"."id" user_id,
- "users_user"."full_name" full_name,
- COALESCE("counters".count, 0) count
- FROM users_user
- LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
- WHERE ("users_user"."is_system" IS TRUE)
+
+ SELECT "users_user"."id" user_id,
+ "users_user"."full_name" full_name,
+ "users_user"."username" username,
+ COALESCE("counters".count, 0) count
+ FROM users_user
+ LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
+ WHERE ("users_user"."is_system" IS TRUE)
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -348,11 +352,11 @@ def _get_userstories_owners(project, queryset):
rows = cursor.fetchall()
result = []
- for id, full_name, count in rows:
+ for id, full_name, username, count in rows:
if count > 0:
result.append({
"id": id,
- "full_name": full_name,
+ "full_name": full_name or username or "",
"count": count,
})
return sorted(result, key=itemgetter("full_name"))
From cafd9be2c96ec3f823ff662ed39a36c5214a9aaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 9 Feb 2016 18:37:23 +0100
Subject: [PATCH 030/105] Remove some prints
---
tests/integration/test_totals_projects.py | 1 -
tests/integration/test_users.py | 2 --
2 files changed, 3 deletions(-)
diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py
index 6b46983b..eb4b2803 100644
--- a/tests/integration/test_totals_projects.py
+++ b/tests/integration/test_totals_projects.py
@@ -146,7 +146,6 @@ def test_project_totals_updated_on_like(client):
client.login(project.owner)
url_like = reverse("projects-like", args=(project.id,))
response = client.post(url_like)
- print(response.data)
project = Project.objects.get(id=project.id)
assert project.total_fans == 4
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 0761b3bf..a2dd0f3f 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -227,8 +227,6 @@ def test_change_avatar_with_long_file_name(client):
post_data = {'avatar': avatar}
response = client.post(url, post_data)
- print(response.data)
-
assert response.status_code == 200
From 7df6450fae3cb58213ed12be670afb2263d79b8c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 10 Feb 2016 16:24:07 +0100
Subject: [PATCH 031/105] Fix sample_data command
---
taiga/projects/management/commands/sample_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 02d6ad54..4415313a 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -285,7 +285,7 @@ class Command(BaseCommand):
return self.sd.paragraphs(2, 4)
if type == DATE_TYPE:
return self.sd.future_date(min_distance=0, max_distance=365)
- if type == DATE_URL:
+ if type == URL_TYPE:
return self.sd.choice(URL_CHOICES)
return None
From e43ea107ed798824b1c8c1386dac3a0d9dfd5a9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Wed, 10 Feb 2016 19:45:34 +0100
Subject: [PATCH 032/105] Remove rest framework deprecated decorators
---
taiga/base/decorators.py | 36 ------------------------------------
1 file changed, 36 deletions(-)
diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py
index af496de4..9f7ebe13 100644
--- a/taiga/base/decorators.py
+++ b/taiga/base/decorators.py
@@ -15,8 +15,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import warnings
-
# Rest Framework 2.4 backport some decorators.
@@ -44,37 +42,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs
return func
return decorator
-
-
-def link(**kwargs):
- """
- Used to mark a method on a ViewSet that should be routed for detail GET requests.
- """
- msg = 'link is pending deprecation. Use detail_route instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
-
- def decorator(func):
- func.bind_to_methods = ['get']
- func.detail = True
- func.permission_classes = kwargs.get('permission_classes', [])
- func.kwargs = kwargs
- return func
-
- return decorator
-
-
-def action(methods=['post'], **kwargs):
- """
- Used to mark a method on a ViewSet that should be routed for detail POST requests.
- """
- msg = 'action is pending deprecation. Use detail_route instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
-
- def decorator(func):
- func.bind_to_methods = methods
- func.detail = True
- func.permission_classes = kwargs.get('permission_classes', [])
- func.kwargs = kwargs
- return func
-
- return decorator
From 52a497eb6c3b466bfa97f793be89a7677ecc4e2b Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 10 Feb 2016 10:44:41 +0100
Subject: [PATCH 033/105] Fixingon_new_history_entry signals
---
taiga/webhooks/signal_handlers.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/taiga/webhooks/signal_handlers.py b/taiga/webhooks/signal_handlers.py
index 1b4db0a6..42755553 100644
--- a/taiga/webhooks/signal_handlers.py
+++ b/taiga/webhooks/signal_handlers.py
@@ -45,7 +45,11 @@ def on_new_history_entry(sender, instance, created, **kwargs):
model = history_service.get_model_from_key(instance.key)
pk = history_service.get_pk_from_key(instance.key)
- obj = model.objects.get(pk=pk)
+ try:
+ obj = model.objects.get(pk=pk)
+ except model.DoesNotExist:
+ # Catch simultaneous DELETE request
+ return None
webhooks = _get_project_webhooks(obj.project)
From 02013f62221b1eb9e6570bd3a4d98ac1bd756393 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 10 Feb 2016 10:57:36 +0100
Subject: [PATCH 034/105] Detecting invalid values when filtering by watchers
---
taiga/base/filters.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index 62b4dc20..a2932551 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -435,8 +435,12 @@ class WatchersFilter(FilterBackend):
if query_watchers:
WatchedModel = apps.get_model("notifications", "Watched")
watched_type = ContentType.objects.get_for_model(queryset.model)
- watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
- queryset = queryset.filter(id__in=watched_ids)
+
+ try:
+ watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
+ queryset = queryset.filter(id__in=watched_ids)
+ except ValueError:
+ raise exc.BadRequest(_("Error in filter params types."))
return super().filter_queryset(request, queryset, view)
From a206d41ba16b361d63abe9f6bacdf4c510c6f3b4 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 10 Feb 2016 11:17:38 +0100
Subject: [PATCH 035/105] Fixing gitlab requests with empty payloads
---
taiga/hooks/gitlab/api.py | 2 +-
tests/integration/test_hooks_gitlab.py | 17 +++++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py
index 6dd2368d..8b3671d0 100644
--- a/taiga/hooks/gitlab/api.py
+++ b/taiga/hooks/gitlab/api.py
@@ -79,4 +79,4 @@ class GitLabViewSet(BaseWebhookApiViewSet):
def _get_event_name(self, request):
payload = json.loads(request.body.decode("utf-8"))
- return payload.get('object_kind', 'push')
+ return payload.get('object_kind', 'push') if payload is not None else 'empty'
diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py
index de53bff6..76e3c3be 100644
--- a/tests/integration/test_hooks_gitlab.py
+++ b/tests/integration/test_hooks_gitlab.py
@@ -60,6 +60,23 @@ def test_ok_signature(client):
assert response.status_code == 204
+def test_ok_empty_payload(client):
+ project = f.ProjectFactory()
+ f.ProjectModulesConfigFactory(project=project, config={
+ "gitlab": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["111.111.111.111"],
+ }
+ })
+
+ url = reverse("gitlab-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = {}
+ response = client.post(url,"null", content_type="application/json", REMOTE_ADDR="111.111.111.111")
+
+ assert response.status_code == 204
+
+
def test_ok_signature_ip_in_network(client):
project = f.ProjectFactory()
f.ProjectModulesConfigFactory(project=project, config={
From f183640e337a6a37668c38332524b2db9e297b24 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 10 Feb 2016 14:20:30 +0100
Subject: [PATCH 036/105] Fixing bad origin configuration for gitlab and
bitbucket
---
taiga/hooks/bitbucket/api.py | 2 +-
taiga/hooks/gitlab/api.py | 2 +-
tests/integration/test_hooks_bitbucket.py | 22 ++++++++++++++++++++++
tests/integration/test_hooks_gitlab.py | 23 +++++++++++++++++++++++
4 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py
index 6fcfcab5..aa4c9f63 100644
--- a/taiga/hooks/bitbucket/api.py
+++ b/taiga/hooks/bitbucket/api.py
@@ -63,7 +63,7 @@ class BitBucketViewSet(BaseWebhookApiViewSet):
try:
mathching_origin_ip = len(all_matching_cidrs(origin_ip,valid_origin_ips)) > 0
- except AddrFormatError:
+ except(AddrFormatError, ValueError):
mathching_origin_ip = False
if not mathching_origin_ip:
diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py
index 8b3671d0..88067e7a 100644
--- a/taiga/hooks/gitlab/api.py
+++ b/taiga/hooks/gitlab/api.py
@@ -61,7 +61,7 @@ class GitLabViewSet(BaseWebhookApiViewSet):
try:
mathching_origin_ip = len(all_matching_cidrs(origin_ip,valid_origin_ips)) > 0
- except AddrFormatError:
+ except (AddrFormatError, ValueError):
mathching_origin_ip = False
if not mathching_origin_ip:
diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py
index f31a674d..122b84ab 100644
--- a/tests/integration/test_hooks_bitbucket.py
+++ b/tests/integration/test_hooks_bitbucket.py
@@ -81,6 +81,28 @@ def test_ok_signature_ip_in_network(client):
assert response.status_code == 204
+def test_ok_signature_invalid_network(client):
+ project = f.ProjectFactory()
+ f.ProjectModulesConfigFactory(project=project, config={
+ "bitbucket": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["131.103.20.160/27;165.254.145.0/26;104.192.143.0/24"],
+ }
+ })
+
+ url = reverse("bitbucket-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = json.dumps({"push": {"changes": [{"new": {"target": { "message": "test message"}}}]}})
+ response = client.post(url,
+ data,
+ content_type="application/json",
+ HTTP_X_EVENT_KEY="repo:push",
+ REMOTE_ADDR="104.192.143.193")
+
+ assert response.status_code == 400
+ assert "Bad signature" in response.data["_error_message"]
+
+
def test_blocked_project(client):
project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF)
f.ProjectModulesConfigFactory(project=project, config={
diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py
index 76e3c3be..cbebcd04 100644
--- a/tests/integration/test_hooks_gitlab.py
+++ b/tests/integration/test_hooks_gitlab.py
@@ -97,6 +97,29 @@ def test_ok_signature_ip_in_network(client):
assert response.status_code == 204
+def test_ok_signature_invalid_network(client):
+ project = f.ProjectFactory()
+ f.ProjectModulesConfigFactory(project=project, config={
+ "gitlab": {
+ "secret": "tpnIwJDz4e",
+ "valid_origin_ips": ["131.103.20.160/27;165.254.145.0/26;104.192.143.0/24"],
+ }
+ })
+
+ url = reverse("gitlab-hook-list")
+ url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e")
+ data = json.dumps({"push": {"changes": [{"new": {"target": { "message": "test message"}}}]}})
+ response = client.post(url,
+ data,
+ content_type="application/json",
+ HTTP_X_EVENT_KEY="repo:push",
+ REMOTE_ADDR="104.192.143.193")
+
+ assert response.status_code == 400
+ assert "Bad signature" in response.data["_error_message"]
+
+
+
def test_blocked_project(client):
project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF)
f.ProjectModulesConfigFactory(project=project, config={
From 6edaeeae667dd646fde79c769a0d0d3dfd9ed1f2 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 10 Feb 2016 14:34:08 +0100
Subject: [PATCH 037/105] Fixing update_projects_order_in_bulk to check the
memberships exist
---
taiga/projects/services/bulk_update_order.py | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py
index e35f05ad..91b33d34 100644
--- a/taiga/projects/services/bulk_update_order.py
+++ b/taiga/projects/services/bulk_update_order.py
@@ -15,10 +15,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.db import transaction
-from django.db import connection
+from django.db import transaction, connection
+from django.core.exceptions import ObjectDoesNotExist
+
from taiga.projects import models
+from contextlib import suppress
+
+
def update_projects_order_in_bulk(bulk_data:list, field:str, user):
"""
Update the order of user projects in the user membership.
@@ -30,9 +34,10 @@ def update_projects_order_in_bulk(bulk_data:list, field:str, user):
new_order_values = []
for membership_data in bulk_data:
project_id = membership_data["project_id"]
- membership = user.memberships.get(project_id=project_id)
- membership_ids.append(membership.id)
- new_order_values.append({field: membership_data["order"]})
+ with suppress(ObjectDoesNotExist):
+ membership = user.memberships.get(project_id=project_id)
+ membership_ids.append(membership.id)
+ new_order_values.append({field: membership_data["order"]})
from taiga.base.utils import db
From dba0597c9c5d67fb48867b037f99956843d21a50 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 9 Feb 2016 14:43:16 +0100
Subject: [PATCH 038/105] Status and default status can be None
---
taiga/projects/history/freeze_impl.py | 6 +++---
taiga/projects/issues/models.py | 2 +-
taiga/projects/issues/services.py | 2 +-
taiga/projects/issues/signals.py | 2 ++
taiga/projects/milestones/services.py | 2 +-
taiga/projects/services/stats.py | 2 +-
taiga/projects/tasks/serializers.py | 2 +-
taiga/projects/tasks/services.py | 4 ++--
taiga/projects/tasks/signals.py | 2 ++
taiga/projects/userstories/services.py | 6 +++---
tests/integration/test_issues.py | 21 +++++++++++++++++++++
tests/integration/test_tasks.py | 13 +++++++++++++
tests/integration/test_userstories.py | 13 +++++++++++++
13 files changed, 64 insertions(+), 13 deletions(-)
diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py
index ba0c10fe..07de4bfc 100644
--- a/taiga/projects/history/freeze_impl.py
+++ b/taiga/projects/history/freeze_impl.py
@@ -266,7 +266,7 @@ def userstory_freezer(us) -> dict:
snapshot = {
"ref": us.ref,
"owner": us.owner_id,
- "status": us.status_id,
+ "status": us.status.id if us.status else None,
"is_closed": us.is_closed,
"finish_date": str(us.finish_date),
"backlog_order": us.backlog_order,
@@ -297,7 +297,7 @@ def issue_freezer(issue) -> dict:
snapshot = {
"ref": issue.ref,
"owner": issue.owner_id,
- "status": issue.status_id,
+ "status": issue.status.id if issue.status else None,
"priority": issue.priority_id,
"severity": issue.severity_id,
"type": issue.type_id,
@@ -321,7 +321,7 @@ def task_freezer(task) -> dict:
snapshot = {
"ref": task.ref,
"owner": task.owner_id,
- "status": task.status_id,
+ "status": task.status.id if task.status else None,
"milestone": task.milestone_id,
"subject": task.subject,
"description": task.description,
diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py
index 07fd2247..3faac676 100644
--- a/taiga/projects/issues/models.py
+++ b/taiga/projects/issues/models.py
@@ -98,4 +98,4 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
@property
def is_closed(self):
- return self.status.is_closed
+ return self.status is not None and self.status.is_closed
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 6d1871e3..6da95c9b 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -117,7 +117,7 @@ def issues_to_csv(project, queryset):
"owner_full_name": issue.owner.get_full_name() if issue.owner else None,
"assigned_to": issue.assigned_to.username if issue.assigned_to else None,
"assigned_to_full_name": issue.assigned_to.get_full_name() if issue.assigned_to else None,
- "status": issue.status.name,
+ "status": issue.status.name if issue.status else None,
"severity": issue.severity.name,
"priority": issue.priority.name,
"type": issue.type.name,
diff --git a/taiga/projects/issues/signals.py b/taiga/projects/issues/signals.py
index a46bfe35..b92418ee 100644
--- a/taiga/projects/issues/signals.py
+++ b/taiga/projects/issues/signals.py
@@ -23,6 +23,8 @@ from django.utils import timezone
####################################
def set_finished_date_when_edit_issue(sender, instance, **kwargs):
+ if instance.status is None:
+ return
if instance.status.is_closed and not instance.finished_date:
instance.finished_date = timezone.now()
elif not instance.status.is_closed and instance.finished_date:
diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py
index 785a8bc7..a717e04f 100644
--- a/taiga/projects/milestones/services.py
+++ b/taiga/projects/milestones/services.py
@@ -23,7 +23,7 @@ from . import models
def calculate_milestone_is_closed(milestone):
return (milestone.user_stories.all().count() > 0 and
- all([task.status.is_closed for task in milestone.tasks.all()]) and
+ all([task.status is not None and task.status.is_closed for task in milestone.tasks.all()]) and
all([user_story.is_closed for user_story in milestone.user_stories.all()]))
diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py
index 44230046..f0d70f1c 100644
--- a/taiga/projects/services/stats.py
+++ b/taiga/projects/services/stats.py
@@ -84,7 +84,7 @@ def get_stats_for_project_issues(project):
)
for issue in issues:
project_issues_stats['total_issues'] += 1
- if issue.status.is_closed:
+ if issue.status is not None and issue.status.is_closed:
project_issues_stats['closed_issues'] += 1
else:
project_issues_stats['opened_issues'] += 1
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index b10ef5a7..5bf022f7 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -68,7 +68,7 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
return mdrender(obj.project, obj.description)
def get_is_closed(self, obj):
- return obj.status.is_closed
+ return obj.status is not None and obj.status.is_closed
class TaskListSerializer(TaskSerializer):
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index 7fad684d..7e3e97ce 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -129,9 +129,9 @@ def tasks_to_csv(project, queryset):
"owner_full_name": task.owner.get_full_name() if task.owner else None,
"assigned_to": task.assigned_to.username if task.assigned_to else None,
"assigned_to_full_name": task.assigned_to.get_full_name() if task.assigned_to else None,
- "status": task.status.name,
+ "status": task.status.name if task.status else None,
"is_iocaine": task.is_iocaine,
- "is_closed": task.status.is_closed,
+ "is_closed": task.status is not None and task.status.is_closed,
"us_order": task.us_order,
"taskboard_order": task.taskboard_order,
"attachments": task.attachments.count(),
diff --git a/taiga/projects/tasks/signals.py b/taiga/projects/tasks/signals.py
index 893869a1..5f1ae65a 100644
--- a/taiga/projects/tasks/signals.py
+++ b/taiga/projects/tasks/signals.py
@@ -100,6 +100,8 @@ def _try_to_close_milestone_when_delete_task(instance):
####################################
def set_finished_date_when_edit_task(sender, instance, **kwargs):
+ if instance.status is None:
+ return
if instance.status.is_closed and not instance.finished_date:
instance.finished_date = timezone.now()
elif not instance.status.is_closed and instance.finished_date:
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index 4270ec54..b8a0533c 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -106,9 +106,9 @@ def calculate_userstory_is_closed(user_story):
return False
if user_story.tasks.count() == 0:
- return user_story.status.is_closed
+ return user_story.status is not None and user_story.status.is_closed
- if all([task.status.is_closed for task in user_story.tasks.all()]):
+ if all([task.status is not None and task.status.is_closed for task in user_story.tasks.all()]):
return True
return False
@@ -179,7 +179,7 @@ def userstories_to_csv(project,queryset):
"owner_full_name": us.owner.get_full_name() if us.owner else None,
"assigned_to": us.assigned_to.username if us.assigned_to else None,
"assigned_to_full_name": us.assigned_to.get_full_name() if us.assigned_to else None,
- "status": us.status.name,
+ "status": us.status.name if us.status else None,
"is_closed": us.is_closed,
"backlog_order": us.backlog_order,
"sprint_order": us.sprint_order,
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index 02551a68..b24e54fb 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -71,6 +71,27 @@ def test_create_issue_without_status(client):
assert response.data['type'] == project.default_issue_type.id
+def test_create_issue_without_status_in_project_without_default_values(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user,
+ default_issue_status=None,
+ default_priority=None,
+ default_severity=None,
+ default_issue_type = None)
+
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url = reverse("issues-list")
+
+ data = {"subject": "Test user story", "project": project.id}
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data['status'] == None
+ assert response.data['severity'] == None
+ assert response.data['priority'] == None
+ assert response.data['type'] == None
+
+
def test_api_create_issues_in_bulk(client):
project = f.create_project()
f.MembershipFactory(project=project, user=project.owner, is_owner=True)
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index 08825955..2fb2fc78 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -53,6 +53,19 @@ def test_create_task_without_status(client):
assert response.data['status'] == project.default_task_status.id
+def test_create_task_without_default_values(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, default_task_status=None)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url = reverse("tasks-list")
+
+ data = {"subject": "Test user story", "project": project.id}
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data['status'] == None
+
+
def test_api_update_task_tags(client):
project = f.ProjectFactory.create()
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 8502c4ca..c50a9710 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -79,6 +79,19 @@ def test_create_userstory_without_status(client):
assert response.data['status'] == project.default_us_status.id
+def test_create_userstory_without_default_values(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, default_us_status=None)
+ f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ url = reverse("userstories-list")
+
+ data = {"subject": "Test user story", "project": project.id}
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data['status'] == None
+
+
def test_api_delete_userstory(client):
us = f.UserStoryFactory.create()
f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
From 7b7548b47dab968d4dcd94e7c7c8b65bd30edf87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 10 Feb 2016 12:47:41 +0100
Subject: [PATCH 039/105] Update django to 1.9 and the rest of requirements
---
requirements-devel.txt | 4 +-
requirements.txt | 33 ++--
settings/common.py | 5 +-
settings/local.py.example | 2 +-
taiga/base/api/fields.py | 8 +-
taiga/base/api/serializers.py | 28 ++--
taiga/base/api/settings.py | 3 +-
taiga/base/api/utils/encoders.py | 3 -
taiga/base/api/views.py | 5 +-
taiga/base/apps.py | 6 +-
taiga/base/management/commands/test_emails.py | 4 +-
taiga/base/response.py | 5 +-
taiga/base/signals/cleanup_files.py | 97 +++++++++++
taiga/base/signals/thumbnails.py | 2 +-
taiga/events/apps.py | 3 +-
taiga/feedback/apps.py | 3 +-
taiga/mdrender/__init__.py | 18 ---
taiga/mdrender/extensions/wikilinks.py | 152 +++++++++---------
taiga/mdrender/service.py | 1 +
taiga/projects/apps.py | 6 +-
taiga/projects/attachments/admin.py | 4 +-
taiga/projects/attachments/models.py | 4 +-
taiga/projects/issues/apps.py | 9 +-
taiga/projects/issues/models.py | 4 +-
taiga/projects/likes/models.py | 4 +-
taiga/projects/notifications/models.py | 5 +-
taiga/projects/notifications/services.py | 4 +-
taiga/projects/references/models.py | 2 +-
taiga/projects/tasks/apps.py | 7 +-
taiga/projects/tasks/models.py | 4 +-
taiga/projects/userstories/apps.py | 89 +++++-----
taiga/projects/userstories/models.py | 6 +-
taiga/projects/votes/models.py | 6 +-
taiga/projects/wiki/models.py | 4 +-
taiga/timeline/apps.py | 9 +-
taiga/timeline/models.py | 2 +-
taiga/webhooks/apps.py | 9 +-
tests/unit/test_serializer_mixins.py | 23 +--
38 files changed, 338 insertions(+), 245 deletions(-)
create mode 100644 taiga/base/signals/cleanup_files.py
diff --git a/requirements-devel.txt b/requirements-devel.txt
index da4f0eb9..e01aa38a 100644
--- a/requirements-devel.txt
+++ b/requirements-devel.txt
@@ -1,8 +1,8 @@
-r requirements.txt
-factory_boy==2.6.0
+factory_boy==2.6.1
py==1.4.31
-pytest==2.8.5
+pytest==2.8.7
pytest-django==2.9.1
pytest-pythonpath==0.7
diff --git a/requirements.txt b/requirements.txt
index 993c3787..181e863f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,37 +1,36 @@
-Django==1.8.6
+Django==1.9.2
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
django-picklefield==0.3.2
-django-sampledatahelper==0.3.0
-gunicorn==19.3.0
+django-sampledatahelper==0.4.0
+gunicorn==19.4.5
psycopg2==2.6.1
-Pillow==2.9.0
+Pillow==3.1.1
pytz==2015.7
six==1.10.0
-amqp==1.4.7
-djmail==0.11
+amqp==1.4.9
+djmail==0.12.0.post1
django-pgjson==0.3.1
djorm-pgarray==1.2
-django-jinja==2.1.1
+django-jinja==2.1.2
jinja2==2.8
pygments==2.0.2
-django-sites==0.8
+django-sites==0.9
Markdown==2.6.5
fn==0.4.3
diff-match-patch==20121119
-requests==2.8.1
+requests==2.9.1
django-sr==0.0.4
-easy-thumbnails==2.2.1
-celery==3.1.19
+easy-thumbnails==2.3
+celery==3.1.20
redis==2.10.5
-Unidecode==0.04.18
-raven==5.9.2
+Unidecode==0.04.19
+raven==5.10.2
bleach==1.4.2
-django-ipware==1.1.2
-premailer==2.9.6
+django-ipware==1.1.3
+premailer==2.9.7
cssutils==1.0.1 # Compatible with python 3.5
-django-transactional-cleanup==0.1.15
lxml==3.5.0
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
-pyjwkest==1.0.9
+pyjwkest==1.1.5
python-dateutil==2.4.2
netaddr==0.7.18
diff --git a/settings/common.py b/settings/common.py
index 7096f6e6..e207f057 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -30,7 +30,7 @@ DEBUG = False
DATABASES = {
"default": {
- "ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
+ "ENGINE": "django.db.backends.postgresql",
"NAME": "taiga",
}
}
@@ -320,7 +320,6 @@ INSTALLED_APPS = [
"sr",
"easy_thumbnails",
"raven.contrib.django.raven_compat",
- "django_transactional_cleanup",
]
WSGI_APPLICATION = "taiga.wsgi.application"
@@ -347,7 +346,7 @@ LOGGING = {
"handlers": {
"null": {
"level":"DEBUG",
- "class":"django.utils.log.NullHandler",
+ "class":"logging.NullHandler",
},
"console":{
"level":"DEBUG",
diff --git a/settings/local.py.example b/settings/local.py.example
index 28f5abb9..d0052032 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -24,7 +24,7 @@ from .development import *
DATABASES = {
'default': {
- 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2',
+ 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'taiga',
'USER': 'taiga',
'PASSWORD': 'changeme',
diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py
index 50fe58f1..365e4070 100644
--- a/taiga/base/api/fields.py
+++ b/taiga/base/api/fields.py
@@ -64,17 +64,17 @@ from django.utils.encoding import is_protected_type
from django.utils.functional import Promise
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
-from django.utils.datastructures import SortedDict
from . import ISO_8601
from .settings import api_settings
+from collections import OrderedDict
+from decimal import Decimal, DecimalException
import copy
import datetime
import inspect
import re
import warnings
-from decimal import Decimal, DecimalException
def is_non_str_iterable(obj):
@@ -255,7 +255,7 @@ class Field(object):
return [self.to_native(item) for item in value]
elif isinstance(value, dict):
# Make sure we preserve field ordering, if it exists
- ret = SortedDict()
+ ret = OrderedDict()
for key, val in value.items():
ret[key] = self.to_native(val)
return ret
@@ -270,7 +270,7 @@ class Field(object):
return {}
def metadata(self):
- metadata = SortedDict()
+ metadata = OrderedDict()
metadata["type"] = self.type_label
metadata["required"] = getattr(self, "required", False)
optional_attrs = ["read_only", "label", "help_text",
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index 8216ddf4..55ae824f 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -59,11 +59,11 @@ from django.core.paginator import Page
from django.db import models
from django.forms import widgets
from django.utils import six
-from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
from .settings import api_settings
+from collections import OrderedDict
import copy
import datetime
import inspect
@@ -148,7 +148,7 @@ class DictWithMetadata(dict):
return dict(self)
-class SortedDictWithMetadata(SortedDict):
+class OrderedDictWithMetadata(OrderedDict):
"""
A sorted dict-like object, that can have additional properties attached.
"""
@@ -158,7 +158,7 @@ class SortedDictWithMetadata(SortedDict):
Overriden to remove the metadata from the dict, since it shouldn't be
pickle and may in some instances be unpickleable.
"""
- return SortedDict(self).__dict__
+ return OrderedDict(self).__dict__
def _is_protected_type(obj):
@@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs):
if hasattr(base, "base_fields"):
fields = list(base.base_fields.items()) + fields
- return SortedDict(fields)
+ return OrderedDict(fields)
class SerializerMetaclass(type):
@@ -222,7 +222,7 @@ class BaseSerializer(WritableField):
pass
_options_class = SerializerOptions
- _dict_class = SortedDictWithMetadata
+ _dict_class = OrderedDictWithMetadata
def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=None,
@@ -268,7 +268,7 @@ class BaseSerializer(WritableField):
This will be the set of any explicitly declared fields,
plus the set of fields returned by get_default_fields().
"""
- ret = SortedDict()
+ ret = OrderedDict()
# Get the explicitly declared fields
base_fields = copy.deepcopy(self.base_fields)
@@ -284,7 +284,7 @@ class BaseSerializer(WritableField):
# If "fields" is specified, use those fields, in that order.
if self.opts.fields:
assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple"
- new = SortedDict()
+ new = OrderedDict()
for key in self.opts.fields:
new[key] = ret[key]
ret = new
@@ -458,7 +458,10 @@ class BaseSerializer(WritableField):
many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type))
if many:
- return [self.to_native(item) for item in value]
+ try:
+ return [self.to_native(item) for item in value]
+ except TypeError:
+ pass # LazyObject is iterable so we need to catch this
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
@@ -610,7 +613,10 @@ class BaseSerializer(WritableField):
DeprecationWarning, stacklevel=2)
if many:
- self._data = [self.to_native(item) for item in obj]
+ try:
+ self._data = [self.to_native(item) for item in obj]
+ except TypeError:
+ self._data = self.to_native(obj) # LazyObject is iterable so we need to catch this
else:
self._data = self.to_native(obj)
@@ -645,7 +651,7 @@ class BaseSerializer(WritableField):
Useful for things like responding to OPTIONS requests, or generating
API schemas for auto-documentation.
"""
- return SortedDict(
+ return OrderedDict(
[(field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)]
)
@@ -740,7 +746,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer)))
assert cls is not None, \
"Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__
opts = cls._meta.concrete_model._meta
- ret = SortedDict()
+ ret = OrderedDict()
nested = bool(self.opts.depth)
# Deal with adding the primary key field
diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py
index bada39b7..9b894be6 100644
--- a/taiga/base/api/settings.py
+++ b/taiga/base/api/settings.py
@@ -62,9 +62,10 @@ back to the defaults.
from __future__ import unicode_literals
from django.conf import settings
-from django.utils import importlib
from django.utils import six
+import importlib
+
from . import ISO_8601
diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py
index cf792697..0f878914 100644
--- a/taiga/base/api/utils/encoders.py
+++ b/taiga/base/api/utils/encoders.py
@@ -45,13 +45,10 @@
Helper classes for parsers.
"""
from django.db.models.query import QuerySet
-from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from django.utils import timezone
from django.utils.encoding import force_text
-from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
-
import datetime
import decimal
import types
diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py
index 68f58e27..791523d4 100644
--- a/taiga/base/api/views.py
+++ b/taiga/base/api/views.py
@@ -43,6 +43,8 @@
import json
+from collections import OrderedDict
+
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse
@@ -50,7 +52,6 @@ from django.http.response import HttpResponseBase
from django.views.decorators.csrf import csrf_exempt
from django.views.defaults import server_error
from django.views.generic import View
-from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
@@ -462,7 +463,7 @@ class APIView(View):
# By default we can't provide any form-like information, however the
# generic views override this implementation and add additional
# information for POST and PUT methods, based on the serializer.
- ret = SortedDict()
+ ret = OrderedDict()
ret['name'] = self.get_view_name()
ret['description'] = self.get_view_description()
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
diff --git a/taiga/base/apps.py b/taiga/base/apps.py
index 5a35fa30..b56aaafb 100644
--- a/taiga/base/apps.py
+++ b/taiga/base/apps.py
@@ -17,12 +17,14 @@
from django.apps import AppConfig
-from .signals.thumbnails import connect_thumbnail_signals
-
class BaseAppConfig(AppConfig):
name = "taiga.base"
verbose_name = "Base App Config"
def ready(self):
+ from .signals.thumbnails import connect_thumbnail_signals
+ from .signals.cleanup_files import connect_cleanup_files_signals
+
connect_thumbnail_signals()
+ connect_cleanup_files_signals()
diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py
index 76d9bc2f..a3e99688 100644
--- a/taiga/base/management/commands/test_emails.py
+++ b/taiga/base/management/commands/test_emails.py
@@ -19,7 +19,7 @@ import datetime
from optparse import make_option
-from django.db.models.loading import get_model
+from django.apps import apps
from django.core.management.base import BaseCommand
from django.utils import timezone
@@ -163,7 +163,7 @@ class Command(BaseCommand):
}
for notification_email in notification_emails:
- model = get_model(*notification_email[0].split("."))
+ model = apps.get_model(*notification_email[0].split("."))
snapshot = {
"subject": "Tests subject",
"ref": 123123,
diff --git a/taiga/base/response.py b/taiga/base/response.py
index 3698ca7a..5b84123a 100644
--- a/taiga/base/response.py
+++ b/taiga/base/response.py
@@ -43,9 +43,10 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""The various HTTP responses for use in returning proper HTTP codes."""
+from http.client import responses
+
from django import http
-from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse
from django.utils import six
@@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse):
"""
# TODO: Deprecate and use a template tag instead
# TODO: Status code text for RFC 6585 status codes
- return STATUS_CODE_TEXT.get(self.status_code, '')
+ return responses.get(self.status_code, '')
def __getstate__(self):
"""
diff --git a/taiga/base/signals/cleanup_files.py b/taiga/base/signals/cleanup_files.py
new file mode 100644
index 00000000..0efab210
--- /dev/null
+++ b/taiga/base/signals/cleanup_files.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.apps import apps
+from django.db import models, connection
+from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler
+from django.db.models.signals import pre_save, post_delete
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+from django.dispatch import Signal
+
+cleanup_pre_delete = Signal(providing_args=["file"])
+cleanup_post_delete = Signal(providing_args=["file"])
+
+
+
+def _find_models_with_filefield():
+ result = []
+ for model in apps.get_models():
+ for field in model._meta.fields:
+ if isinstance(field, models.FileField):
+ result.append(model)
+ break
+ return result
+
+
+def _delete_file(file_obj):
+ def delete_from_storage():
+ try:
+ cleanup_pre_delete.send(sender=None, file=file_obj)
+ storage.delete(file_obj.name)
+ cleanup_post_delete.send(sender=None, file=file_obj)
+ except Exception:
+ logger.exception("Unexpected exception while attempting "
+ "to delete old file '%s'".format(file_obj.name))
+
+ storage = file_obj.storage
+ if storage and storage.exists(file_obj.name):
+ connection.on_commit(delete_from_storage)
+
+
+def _get_file_fields(instance):
+ return filter(
+ lambda field: isinstance(field, models.FileField),
+ instance._meta.fields,
+ )
+
+
+def remove_files_on_change(sender, instance, **kwargs):
+ if not instance.pk:
+ return
+
+ try:
+ old_instance = sender.objects.get(pk=instance.pk)
+ except instance.DoesNotExist:
+ return
+
+ for field in _get_file_fields(instance):
+ old_file = getattr(old_instance, field.name)
+ new_file = getattr(instance, field.name)
+
+ if old_file and old_file != new_file:
+ _delete_file(old_file)
+
+
+def remove_files_on_delete(sender, instance, **kwargs):
+ for field in _get_file_fields(instance):
+ file_to_delete = getattr(instance, field.name)
+
+ if file_to_delete:
+ _delete_file(file_to_delete)
+
+
+def connect_cleanup_files_signals():
+ connections = ConnectionHandler()
+ backend = connections[DEFAULT_DB_ALIAS]
+
+ for model in _find_models_with_filefield():
+ pre_save.connect(remove_files_on_change, sender=model)
+ post_delete.connect(remove_files_on_delete, sender=model)
diff --git a/taiga/base/signals/thumbnails.py b/taiga/base/signals/thumbnails.py
index 5709a59b..53bc1bca 100644
--- a/taiga/base/signals/thumbnails.py
+++ b/taiga/base/signals/thumbnails.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django_transactional_cleanup.signals import cleanup_post_delete
+from .cleanup_files import cleanup_post_delete
from easy_thumbnails.files import get_thumbnailer
diff --git a/taiga/events/apps.py b/taiga/events/apps.py
index 13da3d83..6b4c6a59 100644
--- a/taiga/events/apps.py
+++ b/taiga/events/apps.py
@@ -19,15 +19,16 @@ import sys
from django.apps import AppConfig
from django.db.models import signals
-from . import signal_handlers as handlers
def connect_events_signals():
+ from . import signal_handlers as handlers
signals.post_save.connect(handlers.on_save_any_model, dispatch_uid="events_change")
signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete")
def disconnect_events_signals():
+ from . import signal_handlers as handlers
signals.post_save.disconnect(dispatch_uid="events_change")
signals.post_delete.disconnect(dispatch_uid="events_delete")
diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py
index 505b924b..b48a3cfb 100644
--- a/taiga/feedback/apps.py
+++ b/taiga/feedback/apps.py
@@ -20,8 +20,6 @@ from django.apps import apps
from django.conf import settings
from django.conf.urls import include, url
-from .routers import router
-
class FeedbackAppConfig(AppConfig):
name = "taiga.feedback"
@@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig):
def ready(self):
if settings.FEEDBACK_ENABLED:
from taiga.urls import urlpatterns
+ from .routers import router
urlpatterns.append(url(r'^api/v1/', include(router.urls)))
diff --git a/taiga/mdrender/__init__.py b/taiga/mdrender/__init__.py
index 11dbddfa..e69de29b 100644
--- a/taiga/mdrender/__init__.py
+++ b/taiga/mdrender/__init__.py
@@ -1,18 +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 .service import *
diff --git a/taiga/mdrender/extensions/wikilinks.py b/taiga/mdrender/extensions/wikilinks.py
index d3ac7030..61c36d5c 100644
--- a/taiga/mdrender/extensions/wikilinks.py
+++ b/taiga/mdrender/extensions/wikilinks.py
@@ -7,85 +7,85 @@
# 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 markdown import Extension
-from markdown.inlinepatterns import Pattern
-from markdown.treeprocessors import Treeprocessor
-
-from markdown.util import etree
-
-from taiga.front.templatetags.functions import resolve
-from taiga.base.utils.slug import slugify
-
-import re
-
-
-class WikiLinkExtension(Extension):
- def __init__(self, project, *args, **kwargs):
- self.project = project
- return super().__init__(*args, **kwargs)
-
- def extendMarkdown(self, md, md_globals):
- WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
- md.inlinePatterns.add("wikilinks",
- WikiLinksPattern(md, WIKILINK_RE, self.project),
- " .
from django.contrib import admin
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.admin import GenericTabularInline
from . import models
@@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
-class AttachmentInline(generic.GenericTabularInline):
+class AttachmentInline(GenericTabularInline):
model = models.Attachment
fields = ("attached_file", "owner")
extra = 0
diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py
index 9b01cfca..fa4b337e 100644
--- a/taiga/projects/attachments/models.py
+++ b/taiga/projects/attachments/models.py
@@ -24,7 +24,7 @@ from unidecode import unidecode
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericForeignKey
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.translation import ugettext_lazy as _
@@ -57,7 +57,7 @@ class Attachment(models.Model):
verbose_name=_("content type"))
object_id = models.PositiveIntegerField(null=False, blank=False,
verbose_name=_("object id"))
- content_object = generic.GenericForeignKey("content_type", "object_id")
+ content_object = GenericForeignKey("content_type", "object_id")
created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"),
default=timezone.now)
diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py
index 7c9352f7..26d58b18 100644
--- a/taiga/projects/issues/apps.py
+++ b/taiga/projects/issues/apps.py
@@ -19,12 +19,11 @@ from django.apps import AppConfig
from django.apps import apps
from django.db.models import signals
-from taiga.projects import signals as generic_handlers
-from taiga.projects.custom_attributes import signals as custom_attributes_handlers
-from . import signals as handlers
-
def connect_issues_signals():
+ from taiga.projects import signals as generic_handlers
+ from . import signals as handlers
+
# Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
sender=apps.get_model("issues", "Issue"),
@@ -43,6 +42,8 @@ def connect_issues_signals():
def connect_issues_custom_attributes_signals():
+ from taiga.projects.custom_attributes import signals as custom_attributes_handlers
+
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="create_custom_attribute_value_when_create_issue")
diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py
index 3faac676..df11f671 100644
--- a/taiga/projects/issues/models.py
+++ b/taiga/projects/issues/models.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from django.db import models
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils import timezone
from django.dispatch import receiver
@@ -63,7 +63,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
default=None, related_name="issues_assigned_to_me",
verbose_name=_("assigned to"))
- attachments = generic.GenericRelation("attachments.Attachment")
+ attachments = GenericRelation("attachments.Attachment")
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
_importing = None
diff --git a/taiga/projects/likes/models.py b/taiga/projects/likes/models.py
index d5c4119f..078f48e3 100644
--- a/taiga/projects/likes/models.py
+++ b/taiga/projects/likes/models.py
@@ -17,7 +17,7 @@
# along with this program. If not, see .
from django.conf import settings
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _
class Like(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey("content_type", "object_id")
+ content_object = GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="likes", verbose_name=_("user"))
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,
diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py
index ef56ace7..8eb0db27 100644
--- a/taiga/projects/notifications/models.py
+++ b/taiga/projects/notifications/models.py
@@ -16,7 +16,8 @@
# along with this program. If not, see .
from django.conf import settings
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericForeignKey
+
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
@@ -80,7 +81,7 @@ class HistoryChangeNotification(models.Model):
class Watched(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey("content_type", "object_id")
+ content_object = GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
related_name="watched", verbose_name=_("user"))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index b7223fba..86f5cb27 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -102,13 +102,13 @@ def analize_object_for_watchers(obj:object, comment:str, user:object):
if not hasattr(obj, "add_watcher"):
return
- from taiga import mdrender as mdr
texts = (getattr(obj, "description", ""),
getattr(obj, "content", ""),
comment,)
- _, data = mdr.render_and_extract(obj.get_project(), "\n".join(texts))
+ from taiga.mdrender.service import render_and_extract
+ _, data = render_and_extract(obj.get_project(), "\n".join(texts))
if data["mentions"]:
for user in data["mentions"]:
diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py
index f8b196d9..bf3b072c 100644
--- a/taiga/projects/references/models.py
+++ b/taiga/projects/references/models.py
@@ -18,7 +18,7 @@
from django.db import models
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes.generic import GenericForeignKey
+from django.contrib.contenttypes.fields import GenericForeignKey
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py
index 3284ef1d..ad0209a0 100644
--- a/taiga/projects/tasks/apps.py
+++ b/taiga/projects/tasks/apps.py
@@ -19,11 +19,10 @@ from django.apps import AppConfig
from django.apps import apps
from django.db.models import signals
-from taiga.projects import signals as generic_handlers
-from taiga.projects.custom_attributes import signals as custom_attributes_handlers
-from . import signals as handlers
def connect_tasks_signals():
+ from taiga.projects import signals as generic_handlers
+ from . import signals as handlers
# Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
sender=apps.get_model("tasks", "Task"),
@@ -40,6 +39,7 @@ def connect_tasks_signals():
dispatch_uid="update_project_tags_when_delete_tagglabe_item_task")
def connect_tasks_close_or_open_us_and_milestone_signals():
+ from . import signals as handlers
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_task,
sender=apps.get_model("tasks", "Task"),
@@ -53,6 +53,7 @@ def connect_tasks_close_or_open_us_and_milestone_signals():
dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
def connect_tasks_custom_attributes_signals():
+ from taiga.projects.custom_attributes import signals as custom_attributes_handlers
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task,
sender=apps.get_model("tasks", "Task"),
dispatch_uid="create_custom_attribute_value_when_create_task")
diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py
index 0340f8d6..a1e945f1 100644
--- a/taiga/projects/tasks/models.py
+++ b/taiga/projects/tasks/models.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from django.db import models
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
@@ -62,7 +62,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
default=None, related_name="tasks_assigned_to_me",
verbose_name=_("assigned to"))
- attachments = generic.GenericRelation("attachments.Attachment")
+ attachments = GenericRelation("attachments.Attachment")
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is iocaine"))
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py
index c86f6a99..3586ee57 100644
--- a/taiga/projects/userstories/apps.py
+++ b/taiga/projects/userstories/apps.py
@@ -19,51 +19,50 @@ from django.apps import AppConfig
from django.apps import apps
from django.db.models import signals
-from taiga.projects import signals as generic_handlers
-from taiga.projects.custom_attributes import signals as custom_attributes_handlers
-from . import signals as handlers
-
def connect_userstories_signals():
- # Cached prev object version
- signals.pre_save.connect(handlers.cached_prev_us,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="cached_prev_us")
+ from taiga.projects import signals as generic_handlers
+ from . import signals as handlers
+ # Cached prev object version
+ signals.pre_save.connect(handlers.cached_prev_us,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="cached_prev_us")
- # Role Points
- signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_role_points_when_create_or_edit_us")
+ # Role Points
+ signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_role_points_when_create_or_edit_us")
- # Tasks
- signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_milestone_of_tasks_when_edit_us")
+ # Tasks
+ signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_milestone_of_tasks_when_edit_us")
- # Open/Close US and Milestone
- signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
- signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="try_to_close_milestone_when_delete_us")
+ # Open/Close US and Milestone
+ signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
+ signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="try_to_close_milestone_when_delete_us")
- # Tags
- signals.pre_save.connect(generic_handlers.tags_normalization,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="tags_normalization_user_story")
- signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
- signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
+ # Tags
+ signals.pre_save.connect(generic_handlers.tags_normalization,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="tags_normalization_user_story")
+ signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
+ signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
def connect_userstories_custom_attributes_signals():
- signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="create_custom_attribute_value_when_create_user_story")
+ from taiga.projects.custom_attributes import signals as custom_attributes_handlers
+ signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="create_custom_attribute_value_when_create_user_story")
def connect_all_userstories_signals():
@@ -72,18 +71,18 @@ def connect_all_userstories_signals():
def disconnect_userstories_signals():
- signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
- signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us")
- signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
- signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
+ signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
+ signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us")
+ signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
+ signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
def disconnect_userstories_custom_attributes_signals():
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story")
def disconnect_all_userstories_signals():
diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py
index 6c561c07..4a046524 100644
--- a/taiga/projects/userstories/models.py
+++ b/taiga/projects/userstories/models.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from django.db import models
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
@@ -69,7 +69,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
related_name="user_stories", verbose_name=_("status"),
on_delete=models.SET_NULL)
is_closed = models.BooleanField(default=False)
- points = models.ManyToManyField("projects.Points", null=False, blank=False,
+ points = models.ManyToManyField("projects.Points", blank=False,
related_name="userstories", through="RolePoints",
verbose_name=_("points"))
@@ -97,7 +97,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
verbose_name=_("is client requirement"))
team_requirement = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is team requirement"))
- attachments = generic.GenericRelation("attachments.Attachment")
+ attachments = GenericRelation("attachments.Attachment")
generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True,
on_delete=models.SET_NULL,
related_name="generated_user_stories",
diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py
index 85152cbb..03c8976f 100644
--- a/taiga/projects/votes/models.py
+++ b/taiga/projects/votes/models.py
@@ -17,7 +17,7 @@
# along with this program. If not, see .
from django.conf import settings
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _
class Votes(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey("content_type", "object_id")
+ content_object = GenericForeignKey("content_type", "object_id")
count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
class Meta:
@@ -46,7 +46,7 @@ class Votes(models.Model):
class Vote(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey("content_type", "object_id")
+ content_object = GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="votes", verbose_name=_("user"))
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,
diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py
index abbdf44d..c3e20e4e 100644
--- a/taiga/projects/wiki/models.py
+++ b/taiga/projects/wiki/models.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from django.db import models
-from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
@@ -41,7 +41,7 @@ class WikiPage(OCCModelMixin, WatchedModelMixin, models.Model):
default=timezone.now)
modified_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("modified date"))
- attachments = generic.GenericRelation("attachments.Attachment")
+ attachments = GenericRelation("attachments.Attachment")
_importing = None
class Meta:
diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py
index 898e3aa9..3cdfbc8c 100644
--- a/taiga/timeline/apps.py
+++ b/taiga/timeline/apps.py
@@ -19,16 +19,17 @@ from django.apps import AppConfig
from django.apps import apps
from django.db.models import signals
-from . import signals as handlers
-from taiga.projects.history.models import HistoryEntry
-
class TimelineAppConfig(AppConfig):
name = "taiga.timeline"
verbose_name = "Timeline"
def ready(self):
- signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="timeline")
+ from . import signals as handlers
+
+ signals.post_save.connect(handlers.on_new_history_entry,
+ sender=apps.get_model("history", "HistoryEntry"),
+ dispatch_uid="timeline")
signals.pre_save.connect(handlers.create_membership_push_to_timeline,
sender=apps.get_model("projects", "Membership"))
signals.post_delete.connect(handlers.delete_membership_push_to_timeline,
diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py
index d13db447..73eef400 100644
--- a/taiga/timeline/models.py
+++ b/taiga/timeline/models.py
@@ -22,7 +22,7 @@ from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes.generic import GenericForeignKey
+from django.contrib.contenttypes.fields import GenericForeignKey
from taiga.projects.models import Project
diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py
index f6dda872..90890262 100644
--- a/taiga/webhooks/apps.py
+++ b/taiga/webhooks/apps.py
@@ -15,15 +15,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.apps import apps
from django.apps import AppConfig
from django.db.models import signals
-from . import signal_handlers as handlers
-from taiga.projects.history.models import HistoryEntry
-
def connect_webhooks_signals():
- signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="webhooks")
+ from . import signal_handlers as handlers
+ signals.post_save.connect(handlers.on_new_history_entry,
+ sender=apps.get_model("history", "HistoryEntry"),
+ dispatch_uid="webhooks")
def disconnect_webhooks_signals():
diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py
index e9f94e0c..3e656caa 100644
--- a/tests/unit/test_serializer_mixins.py
+++ b/tests/unit/test_serializer_mixins.py
@@ -10,35 +10,36 @@ pytestmark = pytest.mark.django_db(transaction=True)
import factory
-class TestingProjectModel(models.Model):
+class AuxProjectModel(models.Model):
pass
-class TestingModelWithNameAttribute(models.Model):
+class AuxModelWithNameAttribute(models.Model):
name = models.CharField(max_length=255, null=False, blank=False)
- project = models.ForeignKey(TestingProjectModel, null=False, blank=False)
+ project = models.ForeignKey(AuxProjectModel, null=False, blank=False)
-class TestingSerializer(ValidateDuplicatedNameInProjectMixin):
+class AuxSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
- model = TestingModelWithNameAttribute
+ model = AuxModelWithNameAttribute
+
def test_duplicated_name_validation():
- project = TestingProjectModel.objects.create()
- instance_1 = TestingModelWithNameAttribute.objects.create(name="1", project=project)
- instance_2 = TestingModelWithNameAttribute.objects.create(name="2", project=project)
+ project = AuxProjectModel.objects.create()
+ instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project)
+ instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project)
# No duplicated_name
- serializer = TestingSerializer(data={"name": "3", "project": project.id})
+ serializer = AuxSerializer(data={"name": "3", "project": project.id})
assert serializer.is_valid()
# Create duplicated_name
- serializer = TestingSerializer(data={"name": "1", "project": project.id})
+ serializer = AuxSerializer(data={"name": "1", "project": project.id})
assert not serializer.is_valid()
# Update name to existing one
- serializer = TestingSerializer(data={"id": instance_2.id, "name": "1","project": project.id})
+ serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id})
assert not serializer.is_valid()
From c22036f4c6945c399f405423fb142eef06e32df9 Mon Sep 17 00:00:00 2001
From: Amine Zyad
Date: Sat, 13 Feb 2016 21:52:22 +0000
Subject: [PATCH 040/105] Removed 'load url from future' for compatibility with
Django 1.9
---
taiga/base/api/templates/api/base.html | 1 -
taiga/base/api/templates/api/login_base.html | 1 -
2 files changed, 2 deletions(-)
diff --git a/taiga/base/api/templates/api/base.html b/taiga/base/api/templates/api/base.html
index 074b1e9e..26921db5 100644
--- a/taiga/base/api/templates/api/base.html
+++ b/taiga/base/api/templates/api/base.html
@@ -1,4 +1,3 @@
-{% load url from future %}
{% load api %}
diff --git a/taiga/base/api/templates/api/login_base.html b/taiga/base/api/templates/api/login_base.html
index 118fdbc5..96870344 100644
--- a/taiga/base/api/templates/api/login_base.html
+++ b/taiga/base/api/templates/api/login_base.html
@@ -1,4 +1,3 @@
-{% load url from future %}
{% load api %}
From 6553ad34e4a477e0c5f3bbc0bd3522e45e396791 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 12 Feb 2016 20:10:25 +0100
Subject: [PATCH 041/105] Fixing deadlock and condition race errrors
---
taiga/base/api/mixins.py | 8 +++++--
taiga/base/decorators.py | 21 +++++++++++++++++--
taiga/base/utils/db.py | 8 +++++--
taiga/events/signal_handlers.py | 5 +++--
taiga/projects/userstories/apps.py | 11 ++++++++++
taiga/projects/userstories/signals.py | 11 ++++++++++
.../test_storage_resources.py | 4 ++--
tests/integration/test_userstorage_api.py | 5 +----
tests/unit/test_utils.py | 3 +++
9 files changed, 62 insertions(+), 14 deletions(-)
diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py
index e07635ed..1125fcd9 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -54,7 +54,7 @@ from .settings import api_settings
from .utils import get_object_or_404
from .. import exceptions as exc
-
+from ..decorators import model_pk_lock
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
@@ -161,12 +161,15 @@ class UpdateModelMixin:
"""
@tx.atomic
+ @model_pk_lock
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none()
-
self.check_permissions(request, 'update', self.object)
+ if self.object is None:
+ raise Http404
+
serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial)
@@ -227,6 +230,7 @@ class DestroyModelMixin:
Destroy a model instance.
"""
@tx.atomic
+ @model_pk_lock
def destroy(self, request, *args, **kwargs):
obj = self.get_object_or_none()
self.check_permissions(request, 'destroy', obj)
diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py
index 9f7ebe13..afddb058 100644
--- a/taiga/base/decorators.py
+++ b/taiga/base/decorators.py
@@ -15,8 +15,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
-# Rest Framework 2.4 backport some decorators.
+from django_pglocks import advisory_lock
def detail_route(methods=['get'], **kwargs):
"""
@@ -42,3 +41,21 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs
return func
return decorator
+
+
+def model_pk_lock(func):
+ """
+ This decorator is designed to be used in ModelViewsets methods to lock them based
+ on the model and the id of the selected object.
+ """
+ def decorator(self, *args, **kwargs):
+ from taiga.base.utils.db import get_typename_for_model_class
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ pk = self.kwargs.get(self.pk_url_kwarg, None)
+ tn = get_typename_for_model_class(self.get_queryset().model)
+ key = "{0}:{1}".format(tn, pk)
+
+ with advisory_lock(key) as acquired_key_lock:
+ return func(self, *args, **kwargs)
+
+ return decorator
diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py
index 2619a545..2e076021 100644
--- a/taiga/base/utils/db.py
+++ b/taiga/base/utils/db.py
@@ -19,6 +19,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.shortcuts import _get_queryset
+from django_pglocks import advisory_lock
+
from . import functions
import re
@@ -116,7 +118,6 @@ def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
callback(instance)
-@transaction.atomic
def update_in_bulk_with_ids(ids, list_of_new_values, model):
"""Update a table using a list of ids.
@@ -125,8 +126,11 @@ def update_in_bulk_with_ids(ids, list_of_new_values, model):
to the instance in the same index position as the dict.
:param model: Model of the ids.
"""
+ tn = get_typename_for_model_class(model)
for id, new_values in zip(ids, list_of_new_values):
- model.objects.filter(id=id).update(**new_values)
+ key = "{0}:{1}".format(tn, id)
+ with advisory_lock(key) as acquired_key_lock:
+ model.objects.filter(id=id).update(**new_values)
def to_tsquery(term):
diff --git a/taiga/events/signal_handlers.py b/taiga/events/signal_handlers.py
index 360e780d..ac2ec87d 100644
--- a/taiga/events/signal_handlers.py
+++ b/taiga/events/signal_handlers.py
@@ -55,5 +55,6 @@ def on_delete_any_model(sender, instance, **kwargs):
return
sesionid = mw.get_current_session_id()
- events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
-
+
+ emit_event = lambda: events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
+ connection.on_commit(emit_event)
diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py
index 3586ee57..58e0a65c 100644
--- a/taiga/projects/userstories/apps.py
+++ b/taiga/projects/userstories/apps.py
@@ -23,6 +23,17 @@ from django.db.models import signals
def connect_userstories_signals():
from taiga.projects import signals as generic_handlers
from . import signals as handlers
+
+ # When deleting user stories we must disable task signals while delating and
+ # enabling them in the end
+ signals.pre_delete.connect(handlers.disable_task_signals,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid='disable_task_signals')
+
+ signals.post_delete.connect(handlers.enable_tasks_signals,
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid='enable_tasks_signals')
+
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_us,
sender=apps.get_model("userstories", "UserStory"),
diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py
index 8a9c66c0..b5afb105 100644
--- a/taiga/projects/userstories/signals.py
+++ b/taiga/projects/userstories/signals.py
@@ -18,6 +18,17 @@
from contextlib import suppress
from django.core.exceptions import ObjectDoesNotExist
from taiga.projects.history.services import take_snapshot
+from taiga.projects.tasks.apps import connect_all_tasks_signals, disconnect_all_tasks_signals
+
+
+# Enable tasks signals
+def enable_tasks_signals(sender, instance, **kwargs):
+ connect_all_tasks_signals()
+
+
+# Disable tasks signals
+def disable_task_signals(sender, instance, **kwargs):
+ disconnect_all_tasks_signals()
####################################
# Signals for cached prev US
diff --git a/tests/integration/resources_permissions/test_storage_resources.py b/tests/integration/resources_permissions/test_storage_resources.py
index 65c095ed..4bc6de2b 100644
--- a/tests/integration/resources_permissions/test_storage_resources.py
+++ b/tests/integration/resources_permissions/test_storage_resources.py
@@ -61,7 +61,7 @@ def test_storage_update(client, data):
storage_data["key"] = "test"
storage_data = json.dumps(storage_data)
results = helper_test_http_method(client, 'put', url, storage_data, users)
- assert results == [401, 200, 201]
+ assert results == [401, 200, 404]
def test_storage_delete(client, data):
@@ -118,4 +118,4 @@ def test_storage_patch(client, data):
patch_data = json.dumps({"value": {"test": "test-value"}})
results = helper_test_http_method(client, 'patch', url, patch_data, users)
- assert results == [401, 200, 201]
+ assert results == [401, 200, 404]
diff --git a/tests/integration/test_userstorage_api.py b/tests/integration/test_userstorage_api.py
index 66d4dd3b..3c754cad 100644
--- a/tests/integration/test_userstorage_api.py
+++ b/tests/integration/test_userstorage_api.py
@@ -144,10 +144,7 @@ def test_update_entries(client):
assert response.status_code == 404
response = client.json.put(reverse("user-storage-detail", args=[form["key"]]),
json.dumps(form))
- assert response.status_code == 201
- response = client.json.get(reverse("user-storage-detail", args=[form["key"]]))
- assert response.status_code == 200
- assert response.data["value"] == form["value"]
+ assert response.status_code == 404
def test_delete_storage_entry(client):
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index edd4a9e6..2cabc548 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -14,6 +14,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import pytest
from unittest import mock
@@ -23,6 +24,8 @@ import re
from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url
from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids, to_tsquery
+pytestmark = pytest.mark.django_db
+
def test_is_absolute_url():
assert is_absolute_url("http://domain/path")
From deb668d74fb93da7898547d203187312dd1dc238 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 9 Feb 2016 12:41:29 +0100
Subject: [PATCH 042/105] Importing valid project without slug
---
taiga/export_import/api.py | 3 ++-
tests/integration/test_importer_api.py | 18 ++++++++++++++++++
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index bbb812f7..3723dd68 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -231,7 +231,8 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if not enough_slots:
raise exc.BadRequest(not_enough_slots_error)
- if Project.objects.filter(slug=dump['slug']).exists():
+ slug = dump.get('slug', None)
+ if slug is not None and Project.objects.filter(slug=slug).exists():
del dump['slug']
members = len(dump.get("memberships", []))
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 52784d47..1e753727 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -1476,3 +1476,21 @@ def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_
response_data = response.data
assert "id" in response_data
assert response_data["name"] == "Valid project"
+
+
+def test_valid_dump_import_without_slug(client):
+ project = f.ProjectFactory.create(slug="existing-slug")
+ user = f.UserFactory.create()
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "name": "Project name",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
From 26d10ca7a3ac6fdc467d56618e01ec71ec2e3192 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 16 Feb 2016 14:55:35 +0100
Subject: [PATCH 043/105] Fixing attachments with long names
---
taiga/base/utils/files.py | 42 +++++++++++++++++++
taiga/projects/attachments/models.py | 19 +--------
.../projects/migrations/0031_project_logo.py | 2 +-
taiga/projects/models.py | 27 ++----------
taiga/users/models.py | 23 +---------
tests/integration/test_attachments.py | 16 +++++++
tests/integration/test_projects.py | 18 ++++++++
7 files changed, 85 insertions(+), 62 deletions(-)
create mode 100644 taiga/base/utils/files.py
diff --git a/taiga/base/utils/files.py b/taiga/base/utils/files.py
new file mode 100644
index 00000000..72214606
--- /dev/null
+++ b/taiga/base/utils/files.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import hashlib
+
+from os import path, urandom
+from unidecode import unidecode
+
+from django.template.defaultfilters import slugify
+from django.utils import timezone
+from django.utils.encoding import force_bytes
+
+from taiga.base.utils.iterators import split_by_n
+
+def get_file_path(instance, filename, base_path):
+ basename = path.basename(filename).lower()
+ base, ext = path.splitext(basename)
+ base = slugify(unidecode(base))[0:100]
+ basename = "".join([base, ext])
+
+ hs = hashlib.sha256()
+ hs.update(force_bytes(timezone.now().isoformat()))
+ hs.update(urandom(1024))
+
+ p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1)
+ hash_part = path.join(p1, p2, p3, p4, "".join(p5))
+
+ return path.join(base_path, hash_part, basename)
diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py
index fa4b337e..daec4a2c 100644
--- a/taiga/projects/attachments/models.py
+++ b/taiga/projects/attachments/models.py
@@ -16,35 +16,20 @@
# along with this program. If not, see .
import hashlib
-import os
-import os.path as path
-
-from unidecode import unidecode
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.utils import timezone
-from django.utils.encoding import force_bytes
from django.utils.translation import ugettext_lazy as _
from django.utils.text import get_valid_filename
-from taiga.base.utils.iterators import split_by_n
+from taiga.base.utils.files import get_file_path
def get_attachment_file_path(instance, filename):
- basename = path.basename(filename)
- basename = get_valid_filename(basename)
-
- hs = hashlib.sha256()
- hs.update(force_bytes(timezone.now().isoformat()))
- hs.update(os.urandom(1024))
-
- p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1)
- hash_part = path.join(p1, p2, p3, p4, "".join(p5))
-
- return path.join("attachments", hash_part, basename)
+ return get_file_path(instance, filename, "attachments")
class Attachment(models.Model):
diff --git a/taiga/projects/migrations/0031_project_logo.py b/taiga/projects/migrations/0031_project_logo.py
index ebf50a9b..7bb1b317 100644
--- a/taiga/projects/migrations/0031_project_logo.py
+++ b/taiga/projects/migrations/0031_project_logo.py
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='project',
name='logo',
- field=models.FileField(null=True, blank=True, upload_to=taiga.projects.models.get_user_file_path, verbose_name='logo', max_length=500),
+ field=models.FileField(null=True, blank=True, upload_to=taiga.projects.models.get_project_logo_file_path, verbose_name='logo', max_length=500),
),
]
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index d85feb95..78ac8719 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -15,14 +15,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import hashlib
-import os
-import os.path as path
import itertools
import uuid
-from unidecode import unidecode
-
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
@@ -32,8 +27,6 @@ from django.conf import settings
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
-from django.template.defaultfilters import slugify
-from django.utils.encoding import force_bytes
from django.utils import timezone
from django_pgjson.fields import JsonField
@@ -41,7 +34,7 @@ from djorm_pgarray.fields import TextArrayField
from taiga.base.tags import TaggedMixin
from taiga.base.utils.dicts import dict_sum
-from taiga.base.utils.iterators import split_by_n
+from taiga.base.utils.files import get_file_path
from taiga.base.utils.sequence import arithmetic_progression
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.slug import slugify_uniquely_for_queryset
@@ -62,20 +55,8 @@ from . import choices
from dateutil.relativedelta import relativedelta
-def get_user_file_path(instance, filename):
- basename = path.basename(filename).lower()
- base, ext = path.splitext(basename)
- base = slugify(unidecode(base))
- basename = "".join([base, ext])
-
- hs = hashlib.sha256()
- hs.update(force_bytes(timezone.now().isoformat()))
- hs.update(os.urandom(1024))
-
- p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1)
- hash_part = path.join(p1, p2, p3, p4, "".join(p5))
-
- return path.join("project", hash_part, basename)
+def get_project_logo_file_path(instance, filename):
+ return get_file_path(instance, filename, "project")
class Membership(models.Model):
@@ -167,7 +148,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
description = models.TextField(null=False, blank=False,
verbose_name=_("description"))
- logo = models.FileField(upload_to=get_user_file_path,
+ logo = models.FileField(upload_to=get_project_logo_file_path,
max_length=500, null=True, blank=True,
verbose_name=_("logo"))
diff --git a/taiga/users/models.py b/taiga/users/models.py
index e55bf72c..b4d960c2 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -15,15 +15,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import hashlib
-import os
-import os.path as path
import random
import re
import uuid
-from unidecode import unidecode
-
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
@@ -33,15 +28,13 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import UserManager, AbstractBaseUser
from django.core import validators
from django.utils import timezone
-from django.utils.encoding import force_bytes
-from django.template.defaultfilters import slugify
from django_pgjson.fields import JsonField
from djorm_pgarray.fields import TextArrayField
from taiga.auth.tokens import get_token_for_user
from taiga.base.utils.slug import slugify_uniquely
-from taiga.base.utils.iterators import split_by_n
+from taiga.base.utils.files import get_file_path
from taiga.permissions.permissions import MEMBERS_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel
@@ -54,19 +47,7 @@ def generate_random_hex_color():
def get_user_file_path(instance, filename):
- basename = path.basename(filename).lower()
- base, ext = path.splitext(basename)
- base = slugify(unidecode(base))[0:100]
- basename = "".join([base, ext])
-
- hs = hashlib.sha256()
- hs.update(force_bytes(timezone.now().isoformat()))
- hs.update(os.urandom(1024))
-
- p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1)
- hash_part = path.join(p1, p2, p3, p4, "".join(p5))
-
- return path.join("user", hash_part, basename)
+ return get_file_path(instance, filename, "user")
class PermissionsMixin(models.Model):
diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py
index 54844385..4234cd81 100644
--- a/tests/integration/test_attachments.py
+++ b/tests/integration/test_attachments.py
@@ -45,3 +45,19 @@ def test_create_attachment_on_wrong_project(client):
client.login(issue1.owner)
response = client.post(url, data)
assert response.status_code == 400
+
+
+def test_create_attachment_with_long_file_name(client):
+ issue1 = f.create_issue()
+ f.MembershipFactory(project=issue1.project, user=issue1.owner, is_owner=True)
+
+ url = reverse("issue-attachments-list")
+
+ data = {"description": "test",
+ "object_id": issue1.pk,
+ "project": issue1.project.id,
+ "attached_file": SimpleUploadedFile(500*"x"+".txt", b"test")}
+
+ client.login(issue1.owner)
+ response = client.post(url, data)
+ assert response.data["attached_file"].endswith("/"+100*"x"+".txt")
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 0bbae37a..04b43df7 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -629,6 +629,24 @@ def test_update_project_logo(client):
assert not any(list(map(os.path.exists, original_photo_paths)))
+@pytest.mark.django_db(transaction=True)
+def test_update_project_logo_with_long_file_name(client):
+ user = f.UserFactory.create(is_superuser=True)
+ project = f.create_project()
+ url = reverse("projects-change-logo", args=(project.id,))
+
+ with NamedTemporaryFile(delete=False) as logo:
+ logo.name=500*"x"+".bmp"
+ logo.write(DUMMY_BMP_DATA)
+ logo.seek(0)
+
+ client.login(user)
+ post_data = {'logo': logo}
+ response = client.post(url, post_data)
+
+ assert response.status_code == 200
+
+
@pytest.mark.django_db(transaction=True)
def test_remove_project_logo(client):
user = f.UserFactory.create(is_superuser=True)
From 2c7de59ff87b198106554e4f32bb4fc5b9aa4516 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 17 Feb 2016 08:59:26 +0100
Subject: [PATCH 044/105] Issue 3905: Usernames with dashes in them can't be
@mentioned.
---
taiga/mdrender/extensions/mentions.py | 2 +-
tests/integration/test_mdrender.py | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py
index 83eeae20..2dcea03a 100644
--- a/taiga/mdrender/extensions/mentions.py
+++ b/taiga/mdrender/extensions/mentions.py
@@ -32,7 +32,7 @@ from taiga.users.models import User
class MentionsExtension(Extension):
def extendMarkdown(self, md, md_globals):
- MENTION_RE = r'(@)([a-zA-Z0-9.-\.]+)'
+ MENTION_RE = r'(@)([a-zA-Z0-9.-\._]+)'
mentionsPattern = MentionsPattern(MENTION_RE)
mentionsPattern.md = md
md.inlinePatterns.add('mentions', mentionsPattern, '_end')
diff --git a/tests/integration/test_mdrender.py b/tests/integration/test_mdrender.py
index 91b81ea2..169230db 100644
--- a/tests/integration/test_mdrender.py
+++ b/tests/integration/test_mdrender.py
@@ -38,6 +38,13 @@ def test_proccessor_valid_user_mention():
assert result == expected_result
+def test_proccessor_valid_user_mention_with_dashes():
+ factories.UserFactory(username="user1_text_after_dash", full_name="test name")
+ result = render(dummy_project, "**@user1_text_after_dash**")
+ expected_result = "@user1_text_after_dash
"
+ assert result == expected_result
+
+
def test_proccessor_invalid_user_mention():
result = render(dummy_project, "**@notvaliduser**")
assert result == '@notvaliduser
'
From cc8e5f39b78f2daba3dc3d35c7028f5c681f49ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 15 Feb 2016 14:52:02 +0100
Subject: [PATCH 045/105] Change membership.is_owner to membership.is_admin
---
taiga/base/filters.py | 7 +-
taiga/export_import/api.py | 4 +-
taiga/export_import/dump_service.py | 2 +-
taiga/permissions/permissions.py | 2 +-
taiga/permissions/service.py | 24 ++--
taiga/projects/api.py | 4 +-
taiga/projects/filters.py | 2 +-
.../management/commands/sample_data.py | 4 +-
.../migrations/0038_auto_20160215_1133.py | 20 +++
taiga/projects/models.py | 2 +-
taiga/projects/serializers.py | 5 +-
taiga/projects/services/members.py | 4 +-
taiga/projects/signals.py | 2 +-
taiga/users/services.py | 2 +-
.../test_attachment_resources.py | 8 +-
.../test_history_resources.py | 6 +-
.../test_issues_custom_attributes_resource.py | 8 +-
.../test_issues_resources.py | 8 +-
.../test_milestones_resources.py | 8 +-
.../test_modules_resources.py | 8 +-
.../test_projects_choices_resources.py | 8 +-
.../test_projects_resource.py | 8 +-
.../test_resolver_resources.py | 6 +-
.../test_search_resources.py | 6 +-
.../test_tasks_custom_attributes_resource.py | 8 +-
.../test_tasks_resources.py | 8 +-
.../test_timelines_resources.py | 6 +-
..._userstories_custom_attributes_resource.py | 8 +-
.../test_userstories_resources.py | 8 +-
.../test_webhooks_resources.py | 4 +-
.../test_wiki_resources.py | 8 +-
tests/integration/test_attachments.py | 6 +-
.../test_custom_attributes_issues.py | 16 +--
.../test_custom_attributes_tasks.py | 16 +--
.../test_custom_attributes_user_stories.py | 14 +-
tests/integration/test_exporter_api.py | 6 +-
tests/integration/test_fan_projects.py | 10 +-
tests/integration/test_history.py | 6 +-
tests/integration/test_hooks_bitbucket.py | 6 +-
tests/integration/test_hooks_github.py | 6 +-
tests/integration/test_hooks_gitlab.py | 6 +-
tests/integration/test_importer_api.py | 58 ++++-----
tests/integration/test_issues.py | 6 +-
tests/integration/test_memberships.py | 34 ++---
tests/integration/test_milestones.py | 4 +-
tests/integration/test_notifications.py | 8 +-
tests/integration/test_occ.py | 26 ++--
tests/integration/test_permissions.py | 4 +-
tests/integration/test_projects.py | 120 +++++++++---------
.../integration/test_references_sequences.py | 2 +-
tests/integration/test_roles.py | 2 +-
tests/integration/test_tasks.py | 12 +-
tests/integration/test_throwttling.py | 4 +-
tests/integration/test_totals_projects.py | 2 +-
tests/integration/test_userstories.py | 30 ++---
tests/integration/test_vote_issues.py | 12 +-
tests/integration/test_vote_tasks.py | 12 +-
tests/integration/test_vote_userstories.py | 12 +-
tests/integration/test_watch_issues.py | 12 +-
tests/integration/test_watch_milestones.py | 12 +-
tests/integration/test_watch_projects.py | 14 +-
tests/integration/test_watch_tasks.py | 12 +-
tests/integration/test_watch_userstories.py | 12 +-
tests/integration/test_watch_wikipages.py | 12 +-
64 files changed, 370 insertions(+), 352 deletions(-)
create mode 100644 taiga/projects/migrations/0038_auto_20160215_1133.py
diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index a2932551..ea962af0 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -14,6 +14,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+
import logging
from django.apps import apps
@@ -141,7 +142,7 @@ class PermissionBasedFilterBackend(FilterBackend):
if project_id:
memberships_qs = memberships_qs.filter(project_id=project_id)
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
- Q(is_owner=True))
+ Q(is_admin=True))
projects_list = [membership.project_id for membership in memberships_qs]
@@ -242,7 +243,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
if project_id:
memberships_qs = memberships_qs.filter(project_id=project_id)
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
- Q(is_owner=True))
+ Q(is_admin=True))
projects_list = [membership.project_id for membership in memberships_qs]
@@ -286,7 +287,7 @@ class BaseIsProjectAdminFilterBackend(object):
return []
membership_model = apps.get_model('projects', 'Membership')
- memberships_qs = membership_model.objects.filter(user=request.user, is_owner=True)
+ memberships_qs = membership_model.objects.filter(user=request.user, is_admin=True)
if project_id:
memberships_qs = memberships_qs.filter(project_id=project_id)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index 3723dd68..9eadf5a2 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -127,7 +127,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
try:
owner_membership = project_serialized.object.memberships.get(user=project_serialized.object.owner)
- owner_membership.is_owner = True
+ owner_membership.is_admin = True
owner_membership.save()
except Membership.DoesNotExist:
Membership.objects.create(
@@ -135,7 +135,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
email=project_serialized.object.owner.email,
user=project_serialized.object.owner,
role=project_serialized.object.roles.all().first(),
- is_owner=True
+ is_admin=True
)
# Create project values choicess
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index 6020217f..b41d9534 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -147,7 +147,7 @@ def dict_to_project(data, owner=None):
email=proj.owner.email,
user=proj.owner,
role=proj.roles.all().first(),
- is_owner=True
+ is_admin=True
)
if service.get_errors(clear=False):
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index f543e733..edd24618 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -82,7 +82,7 @@ MEMBERS_PERMISSIONS = [
('delete_wiki_link', _('Delete wiki link')),
]
-OWNERS_PERMISSIONS = [
+ADMINS_PERMISSIONS = [
('modify_project', _('Modify project')),
('add_member', _('Add member')),
('remove_member', _('Remove member')),
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index 5a40ff7e..b452c0b2 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 OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from django.apps import apps
@@ -46,7 +46,7 @@ def is_project_owner(user, obj):
return False
membership = _get_user_project_membership(user, project)
- if membership and membership.is_owner:
+ if membership and membership.is_admin:
return True
return False
@@ -74,43 +74,41 @@ def _get_membership_permissions(membership):
def get_user_project_permissions(user, project):
membership = _get_user_project_membership(user, project)
if user.is_superuser:
- owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS))
+ 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))
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
elif membership:
- if membership.is_owner:
- owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS))
+ if membership.is_admin:
+ admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
else:
- owner_permissions = []
+ admins_permissions = []
members_permissions = []
members_permissions = members_permissions + _get_membership_permissions(membership)
public_permissions = project.public_permissions if project.public_permissions is not None else []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
elif user.is_authenticated():
- owner_permissions = []
+ admins_permissions = []
members_permissions = []
public_permissions = project.public_permissions if project.public_permissions is not None else []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
else:
- owner_permissions = []
+ admins_permissions = []
members_permissions = []
public_permissions = []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
- return set(owner_permissions + members_permissions + public_permissions + anon_permissions)
+ return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
def set_base_permissions_for_project(project):
if project.is_private:
project.anon_permissions = []
project.public_permissions = []
-
else:
- """
- If a project is public anonymous and registered users should have at least visualization permissions
- """
+ # If a project is public anonymous and registered users should have at
+ # least visualization permissions.
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions))
project.public_permissions = list(set((project.public_permissions or []) + anon_permissions))
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 50fe5f46..6da6b1f6 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -353,7 +353,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
# Check the user is an admin membership from the project
try:
- project.memberships.get(is_owner=True, user=user)
+ project.memberships.get(is_admin=True, user=user)
except apps.get_model("projects", "Membership").DoesNotExist:
return response.BadRequest(_("The user must be an admin member of the project"))
@@ -432,7 +432,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.pre_delete(obj)
self.pre_conditions_on_delete(obj)
- obj.delete_related_content()
+ obj.delete_related_content()
obj.delete()
self.post_delete(obj)
return response.NoContent()
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index 5868a6be..b4106a3c 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -70,7 +70,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
if project_id:
memberships_qs = memberships_qs.filter(project_id=project_id)
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=['view_project']) |
- Q(is_owner=True))
+ Q(is_admin=True))
projects_list = [membership.project_id for membership in memberships_qs]
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 4415313a..83d0d7d3 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -150,7 +150,7 @@ class Command(BaseCommand):
Membership.objects.create(email=user.email,
project=project,
role=role,
- is_owner=self.sd.boolean(),
+ is_admin=self.sd.boolean(),
user=user)
if role.computable:
@@ -163,7 +163,7 @@ class Command(BaseCommand):
Membership.objects.create(email=self.sd.email(),
project=project,
role=role,
- is_owner=self.sd.boolean(),
+ is_admin=self.sd.boolean(),
token=''.join(random.sample('abcdef0123456789', 10)))
if role.computable:
diff --git a/taiga/projects/migrations/0038_auto_20160215_1133.py b/taiga/projects/migrations/0038_auto_20160215_1133.py
new file mode 100644
index 00000000..8c374d00
--- /dev/null
+++ b/taiga/projects/migrations/0038_auto_20160215_1133.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-02-15 11:33
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0037_auto_20160208_1751'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='membership',
+ old_name='is_owner',
+ new_name='is_admin',
+ ),
+ ]
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 78ac8719..02129b79 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -70,7 +70,7 @@ class Membership(models.Model):
related_name="memberships")
role = models.ForeignKey("users.Role", null=False, blank=False,
related_name="memberships")
- is_owner = models.BooleanField(default=False, null=False, blank=False)
+ is_admin = models.BooleanField(default=False, null=False, blank=False)
# Invitation metadata
email = models.EmailField(max_length=255, default=None, null=True, blank=True,
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 784a5a8a..0060ac2e 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -181,15 +181,14 @@ class MembershipSerializer(serializers.ModelSerializer):
return attrs
- def validate_is_owner(self, attrs, source):
- is_owner = attrs[source]
+ def validate_is_admin(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
if (self.object and
not services.project_has_valid_admins(project, exclude_user=self.object.user)):
- raise serializers.ValidationError(_("The project must have an owner and at least one of the users must be an active admin"))
+ raise serializers.ValidationError(_("In this project at least one of the users must be an active admin."))
return attrs
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index c883156c..4d6ed0ec 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -40,7 +40,7 @@ def project_has_valid_admins(project, exclude_user=None):
"""
Checks if the project has any owner membership with a user different than the specified
"""
- admin_memberships = project.memberships.filter(is_owner=True, user__is_active=True)
+ admin_memberships = project.memberships.filter(is_admin=True, user__is_active=True)
if exclude_user:
admin_memberships = admin_memberships.exclude(user=exclude_user)
@@ -49,7 +49,7 @@ def project_has_valid_admins(project, exclude_user=None):
def can_user_leave_project(user, project):
membership = project.memberships.get(user=user)
- if not membership.is_owner:
+ if not membership.is_admin:
return True
#The user can't leave if is the real owner of the project
diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py
index afae4b5a..51ff6485 100644
--- a/taiga/projects/signals.py
+++ b/taiga/projects/signals.py
@@ -85,7 +85,7 @@ def project_post_save(sender, instance, created, **kwargs):
if owner_role:
Membership = apps.get_model("projects", "Membership")
Membership.objects.create(user=instance.owner, project=instance, role=owner_role,
- is_owner=True, email=instance.owner.email)
+ is_admin=True, email=instance.owner.email)
## US statuses
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 54ce79d7..b8bed269 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -123,7 +123,7 @@ def get_visible_project_ids(from_user, by_user):
#- The to user is the owner
member_perm_conditions |= \
Q(project__id__in=by_user_project_ids, role__permissions__contains=required_permissions) |\
- Q(project__id__in=by_user_project_ids, is_owner=True)
+ Q(project__id__in=by_user_project_ids, is_admin=True)
Membership = apps.get_model('projects', 'Membership')
#Calculating the user memberships adding the permission filter for the by user
diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index f6cc6339..0395d8b4 100644
--- a/tests/integration/resources_permissions/test_attachment_resources.py
+++ b/tests/integration/resources_permissions/test_attachment_resources.py
@@ -85,19 +85,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
return m
diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py
index 4faf909d..b0991080 100644
--- a/tests/integration/resources_permissions/test_history_resources.py
+++ b/tests/integration/resources_permissions/test_history_resources.py
@@ -64,15 +64,15 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
return m
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 81cfdb31..2c90cbfc 100644
--- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py
@@ -100,19 +100,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project)
m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index 11d3bf62..2a6d3974 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -90,19 +90,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_issue = f.IssueFactory(project=m.public_project,
status__project=m.public_project,
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index b9777ce1..5a6f26a4 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -82,19 +82,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_milestone = f.MilestoneFactory(project=m.public_project)
m.private_milestone1 = f.MilestoneFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py
index 1f77d055..8260bd2f 100644
--- a/tests/integration/resources_permissions/test_modules_resources.py
+++ b/tests/integration/resources_permissions/test_modules_resources.py
@@ -85,19 +85,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
return m
diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py
index 1ffcf87a..207889f9 100644
--- a/tests/integration/resources_permissions/test_projects_choices_resources.py
+++ b/tests/integration/resources_permissions/test_projects_choices_resources.py
@@ -78,19 +78,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index f05b04e5..9bcd7a9f 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -69,19 +69,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
ContentType = apps.get_model("contenttypes", "ContentType")
Project = apps.get_model("projects", "Project")
diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py
index 4f5f4f54..f878ca14 100644
--- a/tests/integration/resources_permissions/test_resolver_resources.py
+++ b/tests/integration/resources_permissions/test_resolver_resources.py
@@ -66,15 +66,15 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.view_only_membership = f.MembershipFactory(project=m.private_project2,
user=m.other_user,
diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py
index 5bca5dc5..8d3d9442 100644
--- a/tests/integration/resources_permissions/test_search_resources.py
+++ b/tests/integration/resources_permissions/test_search_resources.py
@@ -63,15 +63,15 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_issue = f.IssueFactory(project=m.public_project,
status__project=m.public_project,
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 b02a34a6..1fd33e46 100644
--- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py
@@ -100,19 +100,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project)
m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 88a130c8..4771d12c 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -90,19 +90,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
milestone_public_task = f.MilestoneFactory(project=m.public_project)
milestone_private_task1 = f.MilestoneFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py
index e33dc043..0a874443 100644
--- a/tests/integration/resources_permissions/test_timelines_resources.py
+++ b/tests/integration/resources_permissions/test_timelines_resources.py
@@ -63,15 +63,15 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
return m
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 7c951322..9e6bd6ff 100644
--- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
+++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py
@@ -101,19 +101,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project)
m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 3d7a3dd7..80559a87 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -90,19 +90,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py
index 30c72097..5712b5bc 100644
--- a/tests/integration/resources_permissions/test_webhooks_resources.py
+++ b/tests/integration/resources_permissions/test_webhooks_resources.py
@@ -45,10 +45,10 @@ def data():
f.MembershipFactory(project=m.project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.webhook1 = f.WebhookFactory(project=m.project1)
m.webhooklog1 = f.WebhookLogFactory(webhook=m.webhook1)
diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py
index ecbfb83b..c69fb2bc 100644
--- a/tests/integration/resources_permissions/test_wiki_resources.py
+++ b/tests/integration/resources_permissions/test_wiki_resources.py
@@ -84,19 +84,19 @@ def data():
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(project=m.blocked_project,
user=m.project_owner,
- is_owner=True)
+ is_admin=True)
m.public_wiki_page = f.WikiPageFactory(project=m.public_project)
m.private_wiki_page1 = f.WikiPageFactory(project=m.private_project1)
diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py
index 4234cd81..3dee3464 100644
--- a/tests/integration/test_attachments.py
+++ b/tests/integration/test_attachments.py
@@ -13,7 +13,7 @@ def test_create_user_story_attachment_without_file(client):
Bug test "Don't create attachments without attached_file"
"""
us = f.UserStoryFactory.create()
- f.MembershipFactory(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory(project=us.project, user=us.owner, is_admin=True)
attachment_data = {
"description": "test",
"attached_file": None,
@@ -30,7 +30,7 @@ def test_create_user_story_attachment_without_file(client):
def test_create_attachment_on_wrong_project(client):
issue1 = f.create_issue()
issue2 = f.create_issue(owner=issue1.owner)
- f.MembershipFactory(project=issue1.project, user=issue1.owner, is_owner=True)
+ f.MembershipFactory(project=issue1.project, user=issue1.owner, is_admin=True)
assert issue1.owner == issue2.owner
assert issue1.project.owner == issue2.project.owner
@@ -49,7 +49,7 @@ def test_create_attachment_on_wrong_project(client):
def test_create_attachment_with_long_file_name(client):
issue1 = f.create_issue()
- f.MembershipFactory(project=issue1.project, user=issue1.owner, is_owner=True)
+ f.MembershipFactory(project=issue1.project, user=issue1.owner, is_admin=True)
url = reverse("issue-attachments-list")
diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py
index 2e7a675a..7df2a233 100644
--- a/tests/integration/test_custom_attributes_issues.py
+++ b/tests/integration/test_custom_attributes_issues.py
@@ -34,7 +34,7 @@ def test_issue_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.IssueCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("issue-custom-attributes-list")
@@ -51,7 +51,7 @@ def test_issue_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -67,10 +67,10 @@ def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(cl
custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
- is_owner=True)
+ is_admin=True)
url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -94,7 +94,7 @@ def test_issue_custom_attributes_values_update(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -126,7 +126,7 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -151,7 +151,7 @@ def test_issue_custom_attributes_values_delete_issue(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -177,7 +177,7 @@ def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(cli
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project)
diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py
index 0178c62f..9fb1131c 100644
--- a/tests/integration/test_custom_attributes_tasks.py
+++ b/tests/integration/test_custom_attributes_tasks.py
@@ -33,7 +33,7 @@ def test_task_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.TaskCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("task-custom-attributes-list")
@@ -50,7 +50,7 @@ def test_task_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -66,10 +66,10 @@ def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(cli
custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
- is_owner=True)
+ is_admin=True)
url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -93,7 +93,7 @@ def test_task_custom_attributes_values_update(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -124,7 +124,7 @@ def test_task_custom_attributes_values_update_with_error_invalid_key(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -151,7 +151,7 @@ def test_task_custom_attributes_values_delete_task(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -177,7 +177,7 @@ def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(clien
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py
index befda89a..c6a27499 100644
--- a/tests/integration/test_custom_attributes_user_stories.py
+++ b/tests/integration/test_custom_attributes_user_stories.py
@@ -33,7 +33,7 @@ def test_userstory_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.UserStoryCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("userstory-custom-attributes-list")
@@ -50,7 +50,7 @@ def test_userstory_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -66,10 +66,10 @@ def test_userstory_custom_attribute_duplicate_name_error_on_move_between_project
custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
- is_owner=True)
+ is_admin=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
- is_owner=True)
+ is_admin=True)
url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
@@ -93,7 +93,7 @@ def test_userstory_custom_attributes_values_update(client):
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -124,7 +124,7 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
@@ -155,7 +155,7 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
- is_owner=True)
+ is_admin=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py
index 899b1cfb..29578c5f 100644
--- a/tests/integration/test_exporter_api.py
+++ b/tests/integration/test_exporter_api.py
@@ -43,7 +43,7 @@ def test_valid_project_export_with_celery_disabled(client, settings):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("exporter-detail", args=[project.pk])
@@ -59,7 +59,7 @@ def test_valid_project_export_with_celery_enabled(client, settings):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("exporter-detail", args=[project.pk])
@@ -82,7 +82,7 @@ def test_valid_project_with_throttling(client, settings):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("exporter-detail", args=[project.pk])
diff --git a/tests/integration/test_fan_projects.py b/tests/integration/test_fan_projects.py
index 30897948..31e73582 100644
--- a/tests/integration/test_fan_projects.py
+++ b/tests/integration/test_fan_projects.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_like_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-like", args=(project.id,))
client.login(user)
@@ -39,7 +39,7 @@ def test_like_project(client):
def test_unlike_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-unlike", args=(project.id,))
client.login(user)
@@ -51,7 +51,7 @@ def test_unlike_project(client):
def test_list_project_fans(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.LikeFactory.create(content_object=project, user=user)
url = reverse("project-fans-list", args=(project.id,))
@@ -65,7 +65,7 @@ def test_list_project_fans(client):
def test_get_project_fan(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
like = f.LikeFactory.create(content_object=project, user=user)
url = reverse("project-fans-detail", args=(project.id, like.user.id))
@@ -79,7 +79,7 @@ def test_get_project_fan(client):
def test_get_project_is_fan(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url_detail = reverse("projects-detail", args=(project.id,))
url_like = reverse("projects-like", args=(project.id,))
url_unlike = reverse("projects-unlike", args=(project.id,))
diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py
index 23ea50a0..01469c0a 100644
--- a/tests/integration/test_history.py
+++ b/tests/integration/test_history.py
@@ -144,7 +144,7 @@ def test_issue_resource_history_test(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
role = f.RoleFactory.create(project=project)
- f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
issue = f.IssueFactory.create(owner=user, project=project)
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
@@ -201,7 +201,7 @@ def test_take_hidden_snapshot():
def test_history_with_only_comment_shouldnot_be_hidden(client):
project = f.create_project()
us = f.create_userstory(project=project, status__project=project)
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
qs_all = HistoryEntry.objects.all()
qs_hidden = qs_all.filter(is_hidden=True)
@@ -222,7 +222,7 @@ def test_history_with_only_comment_shouldnot_be_hidden(client):
def test_delete_comment_by_project_owner(client):
project = f.create_project()
us = f.create_userstory(project=project)
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ 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",
diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py
index 122b84ab..4494c9f5 100644
--- a/tests/integration/test_hooks_bitbucket.py
+++ b/tests/integration/test_hooks_bitbucket.py
@@ -345,7 +345,7 @@ def test_issues_event_opened_issue(client):
issue.project.default_severity = issue.severity
issue.project.default_priority = issue.priority
issue.project.save()
- Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True)
+ Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True)
notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project)
notify_policy.notify_level = NotifyLevel.all
notify_policy.save()
@@ -551,7 +551,7 @@ def test_issues_event_bad_comment(client):
def test_api_get_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
@@ -566,7 +566,7 @@ def test_api_get_project_modules(client):
def test_api_patch_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py
index 06133ac2..9bede876 100644
--- a/tests/integration/test_hooks_github.py
+++ b/tests/integration/test_hooks_github.py
@@ -260,7 +260,7 @@ def test_issues_event_opened_issue(client):
issue.project.default_severity = issue.severity
issue.project.default_priority = issue.priority
issue.project.save()
- Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True)
+ Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True)
notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project)
notify_policy.notify_level = NotifyLevel.all
notify_policy.save()
@@ -457,7 +457,7 @@ def test_issues_event_bad_comment(client):
def test_api_get_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
@@ -472,7 +472,7 @@ def test_api_get_project_modules(client):
def test_api_patch_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py
index cbebcd04..14156b37 100644
--- a/tests/integration/test_hooks_gitlab.py
+++ b/tests/integration/test_hooks_gitlab.py
@@ -408,7 +408,7 @@ def test_issues_event_opened_issue(client):
issue.project.default_severity = issue.severity
issue.project.default_priority = issue.priority
issue.project.save()
- Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True)
+ Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True)
notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project)
notify_policy.notify_level = NotifyLevel.all
notify_policy.save()
@@ -616,7 +616,7 @@ def test_issues_event_bad_comment(client):
def test_api_get_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
@@ -631,7 +631,7 @@ def test_api_get_project_modules(client):
def test_api_patch_project_modules(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("projects-modules", args=(project.id,))
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 1e753727..5d598de8 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -360,7 +360,7 @@ def test_invalid_project_import_with_custom_attributes(client):
def test_invalid_issue_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-issue", args=[project.pk])
@@ -373,7 +373,7 @@ def test_invalid_issue_import(client):
def test_valid_user_story_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -394,7 +394,7 @@ def test_valid_user_story_import(client):
def test_valid_user_story_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- membership = f.MembershipFactory(project=project, user=user, is_owner=True)
+ membership = f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
custom_attr = f.UserStoryCustomAttributeFactory(project=project)
@@ -418,7 +418,7 @@ def test_valid_user_story_import_with_custom_attributes_values(client):
def test_valid_issue_import_without_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
@@ -441,7 +441,7 @@ def test_valid_issue_import_without_extra_data(client):
def test_valid_issue_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- membership = f.MembershipFactory(project=project, user=user, is_owner=True)
+ membership = f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
@@ -469,7 +469,7 @@ def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
@@ -505,7 +505,7 @@ def test_valid_issue_import_with_extra_data(client):
def test_invalid_issue_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
@@ -530,7 +530,7 @@ def test_invalid_issue_import_with_extra_data(client):
def test_invalid_issue_import_with_bad_choices(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
@@ -590,7 +590,7 @@ def test_invalid_issue_import_with_bad_choices(client):
def test_invalid_us_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-us", args=[project.pk])
@@ -603,7 +603,7 @@ def test_invalid_us_import(client):
def test_valid_us_import_without_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -624,7 +624,7 @@ def test_valid_us_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -655,7 +655,7 @@ def test_valid_us_import_with_extra_data(client):
def test_invalid_us_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -677,7 +677,7 @@ def test_invalid_us_import_with_extra_data(client):
def test_invalid_us_import_with_bad_choices(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -698,7 +698,7 @@ def test_invalid_us_import_with_bad_choices(client):
def test_invalid_task_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-task", args=[project.pk])
@@ -711,7 +711,7 @@ def test_invalid_task_import(client):
def test_valid_task_import_without_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -731,7 +731,7 @@ def test_valid_task_import_without_extra_data(client):
def test_valid_task_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- membership = f.MembershipFactory(project=project, user=user, is_owner=True)
+ membership = f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
custom_attr = f.TaskCustomAttributeFactory(project=project)
@@ -756,7 +756,7 @@ def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -787,7 +787,7 @@ def test_valid_task_import_with_extra_data(client):
def test_invalid_task_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -809,7 +809,7 @@ def test_invalid_task_import_with_extra_data(client):
def test_invalid_task_import_with_bad_choices(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
client.login(user)
@@ -830,7 +830,7 @@ def test_invalid_task_import_with_bad_choices(client):
def test_valid_task_with_user_story(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
us = f.UserStoryFactory.create(project=project)
project.save()
@@ -851,7 +851,7 @@ def test_valid_task_with_user_story(client):
def test_invalid_wiki_page_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-page", args=[project.pk])
@@ -864,7 +864,7 @@ def test_invalid_wiki_page_import(client):
def test_valid_wiki_page_import_without_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-page", args=[project.pk])
@@ -882,7 +882,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-page", args=[project.pk])
@@ -910,7 +910,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
def test_invalid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-page", args=[project.pk])
@@ -930,7 +930,7 @@ def test_invalid_wiki_page_import_with_extra_data(client):
def test_invalid_wiki_link_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-link", args=[project.pk])
@@ -943,7 +943,7 @@ def test_invalid_wiki_link_import(client):
def test_valid_wiki_link_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-wiki-link", args=[project.pk])
@@ -961,7 +961,7 @@ def test_valid_wiki_link_import(client):
def test_invalid_milestone_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-milestone", args=[project.pk])
@@ -975,7 +975,7 @@ def test_valid_milestone_import(client):
user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-milestone", args=[project.pk])
@@ -993,7 +993,7 @@ def test_valid_milestone_import(client):
def test_milestone_import_duplicated_milestone(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("importer-milestone", args=[project.pk])
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index b24e54fb..71f45f3f 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -58,7 +58,7 @@ def test_create_issue_without_status(client):
project.default_severity = severity
project.default_issue_type = type
project.save()
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("issues-list")
data = {"subject": "Test user story", "project": project.id}
@@ -79,7 +79,7 @@ def test_create_issue_without_status_in_project_without_default_values(client):
default_severity=None,
default_issue_type = None)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("issues-list")
data = {"subject": "Test user story", "project": project.id}
@@ -94,7 +94,7 @@ def test_create_issue_without_status_in_project_without_default_values(client):
def test_api_create_issues_in_bulk(client):
project = f.create_project()
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("issues-bulk-create")
diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py
index ec4e3124..6919cb4b 100644
--- a/tests/integration/test_memberships.py
+++ b/tests/integration/test_memberships.py
@@ -34,7 +34,7 @@ def test_api_create_bulk_members(client):
joseph = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester")
gamer = f.RoleFactory(project=project, name="Gamer")
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("memberships-bulk-create")
@@ -57,7 +57,7 @@ def test_api_create_bulk_members_without_enough_memberships_private_project_slot
user = f.UserFactory.create(max_members_private_projects=3)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
url = reverse("memberships-bulk-create")
@@ -81,7 +81,7 @@ def test_api_create_bulk_members_with_enough_memberships_private_project_slots_m
user = f.UserFactory.create(max_members_private_projects=6)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
other_project = f.ProjectFactory(owner=user)
f.MembershipFactory.create(project=other_project)
@@ -110,7 +110,7 @@ def test_api_create_bulk_members_without_enough_memberships_public_project_slots
user = f.UserFactory.create(max_members_public_projects=3)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
url = reverse("memberships-bulk-create")
@@ -134,7 +134,7 @@ def test_api_create_bulk_members_with_enough_memberships_public_project_slots_mu
user = f.UserFactory.create(max_members_public_projects=6)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
other_project = f.ProjectFactory(owner=user)
f.MembershipFactory.create(project=other_project)
@@ -162,7 +162,7 @@ def test_api_create_bulk_members_with_enough_memberships_public_project_slots_mu
def test_api_create_bulk_members_with_extra_text(client, outbox):
project = f.ProjectFactory()
tester = f.RoleFactory(project=project, name="Tester")
- f.MembershipFactory(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory(project=project, user=project.owner, is_admin=True)
url = reverse("memberships-bulk-create")
invitation_extra_text = "this is a not so random invitation text"
@@ -187,7 +187,7 @@ def test_api_create_bulk_members_with_extra_text(client, outbox):
def test_api_resend_invitation(client, outbox):
invitation = f.create_invitation(user=None)
- f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_owner=True)
+ f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_admin=True)
url = reverse("memberships-resend-invitation", kwargs={"pk": invitation.pk})
client.login(invitation.project.owner)
@@ -202,7 +202,7 @@ def test_api_invite_existing_user(client, outbox):
"Should create the invitation linked to that user"
user = f.UserFactory.create()
role = f.RoleFactory.create()
- f.MembershipFactory(project=role.project, user=role.project.owner, is_owner=True)
+ f.MembershipFactory(project=role.project, user=role.project.owner, is_admin=True)
client.login(role.project.owner)
@@ -255,7 +255,7 @@ def test_api_create_invalid_membership_role_doesnt_exist_in_the_project(client):
def test_api_create_membership(client):
- membership = f.MembershipFactory(is_owner=True)
+ membership = f.MembershipFactory(is_admin=True)
role = f.RoleFactory.create(project=membership.project)
user = f.UserFactory.create()
@@ -272,7 +272,7 @@ def test_api_create_membership_without_enough_memberships_private_project_slots_
user = f.UserFactory.create(max_members_private_projects=1)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("memberships-list")
@@ -287,7 +287,7 @@ def test_api_create_membership_with_enough_memberships_private_project_slots_mul
user = f.UserFactory.create(max_members_private_projects=5)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
other_project = f.ProjectFactory(owner=user)
f.MembershipFactory.create(project=other_project)
@@ -307,7 +307,7 @@ def test_api_create_membership_without_enough_memberships_public_project_slots_o
user = f.UserFactory.create(max_members_public_projects=1)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
client.login(user)
url = reverse("memberships-list")
@@ -322,7 +322,7 @@ def test_api_create_membership_with_enough_memberships_public_project_slots_mult
user = f.UserFactory.create(max_members_public_projects=5)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
- f.MembershipFactory(project=project, user=user, is_owner=True)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
other_project = f.ProjectFactory(owner=user)
f.MembershipFactory.create(project=other_project)
@@ -339,7 +339,7 @@ def test_api_create_membership_with_enough_memberships_public_project_slots_mult
def test_api_edit_membership(client):
- membership = f.MembershipFactory(is_owner=True)
+ membership = f.MembershipFactory(is_admin=True)
client.login(membership.user)
url = reverse("memberships-detail", args=[membership.id])
data = {"email": "new@email.com"}
@@ -349,14 +349,14 @@ def test_api_edit_membership(client):
def test_api_delete_membership(client):
- membership = f.MembershipFactory(is_owner=True)
+ membership = f.MembershipFactory(is_admin=True)
client.login(membership.user)
url = reverse("memberships-detail", args=[membership.id])
response = client.json.delete(url)
assert response.status_code == 400
- f.MembershipFactory(is_owner=True, project=membership.project)
+ f.MembershipFactory(is_admin=True, project=membership.project)
url = reverse("memberships-detail", args=[membership.id])
response = client.json.delete(url)
@@ -365,7 +365,7 @@ def test_api_delete_membership(client):
def test_api_delete_membership_without_user(client):
- membership_owner = f.MembershipFactory(is_owner=True)
+ membership_owner = f.MembershipFactory(is_admin=True)
membership_without_user_one = f.MembershipFactory(project=membership_owner.project, user=None)
f.MembershipFactory(project=membership_owner.project, user=None)
client.login(membership_owner.user)
diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py
index e7d6c2b8..4934c324 100644
--- a/tests/integration/test_milestones.py
+++ b/tests/integration/test_milestones.py
@@ -33,7 +33,7 @@ def test_update_milestone_with_userstories_list(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
role = f.RoleFactory.create(project=project)
- f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
sprint = f.MilestoneFactory.create(project=project, owner=user)
f.PointsFactory.create(project=project, value=None)
us = f.UserStoryFactory.create(project=project, owner=user)
@@ -54,7 +54,7 @@ def test_list_milestones_taiga_info_headers(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
role = f.RoleFactory.create(project=project)
- f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
f.MilestoneFactory.create(project=project, owner=user, closed=True)
f.MilestoneFactory.create(project=project, owner=user, closed=True)
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index 42058f07..3a8905a3 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -720,7 +720,7 @@ def test_resource_notification_test(client, settings, mail):
user2 = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user1)
role = f.RoleFactory.create(project=project, permissions=["view_issues"])
- f.MembershipFactory.create(project=project, user=user1, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user1, role=role, is_admin=True)
f.MembershipFactory.create(project=project, user=user2, role=role)
issue = f.IssueFactory.create(owner=user2, project=project)
@@ -758,7 +758,7 @@ def test_watchers_assignation_for_issue(client):
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1)
role2 = f.RoleFactory.create(project=project2)
- f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True)
+ f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
@@ -810,7 +810,7 @@ def test_watchers_assignation_for_task(client):
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1, permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role2 = f.RoleFactory.create(project=project2)
- f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True)
+ f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
@@ -862,7 +862,7 @@ def test_watchers_assignation_for_us(client):
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1)
role2 = f.RoleFactory.create(project=project2)
- f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True)
+ f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
diff --git a/tests/integration/test_occ.py b/tests/integration/test_occ.py
index b8e22223..580f6733 100644
--- a/tests/integration/test_occ.py
+++ b/tests/integration/test_occ.py
@@ -30,7 +30,7 @@ pytestmark = pytest.mark.django_db
def test_valid_us_creation(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
@@ -47,7 +47,7 @@ def test_valid_us_creation(client):
def test_invalid_concurrent_save_for_issue(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
@@ -76,7 +76,7 @@ def test_invalid_concurrent_save_for_issue(client):
def test_valid_concurrent_save_for_issue_different_versions(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
@@ -105,7 +105,7 @@ def test_valid_concurrent_save_for_issue_different_versions(client):
def test_valid_concurrent_save_for_issue_different_fields(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
@@ -134,7 +134,7 @@ def test_valid_concurrent_save_for_issue_different_fields(client):
def test_invalid_concurrent_save_for_wiki_page(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.wiki.api.WikiViewSet.pre_conditions_on_save"
@@ -158,7 +158,7 @@ def test_invalid_concurrent_save_for_wiki_page(client):
def test_valid_concurrent_save_for_wiki_page_different_versions(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.wiki.api.WikiViewSet.pre_conditions_on_save"
@@ -182,7 +182,7 @@ def test_valid_concurrent_save_for_wiki_page_different_versions(client):
def test_invalid_concurrent_save_for_us(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.UserStoryFactory.create(version=10, project=project)
client.login(user)
@@ -209,7 +209,7 @@ def test_invalid_concurrent_save_for_us(client):
def test_valid_concurrent_save_for_us_different_versions(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.userstories.api.UserStoryViewSet.pre_conditions_on_save"
@@ -235,7 +235,7 @@ def test_valid_concurrent_save_for_us_different_versions(client):
def test_valid_concurrent_save_for_us_different_fields(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.userstories.api.UserStoryViewSet.pre_conditions_on_save"
@@ -261,7 +261,7 @@ def test_valid_concurrent_save_for_us_different_fields(client):
def test_invalid_concurrent_save_for_task(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
@@ -287,7 +287,7 @@ def test_invalid_concurrent_save_for_task(client):
def test_valid_concurrent_save_for_task_different_versions(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
@@ -313,7 +313,7 @@ def test_valid_concurrent_save_for_task_different_versions(client):
def test_valid_concurrent_save_for_task_different_fields(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
@@ -340,7 +340,7 @@ def test_valid_concurrent_save_for_task_different_fields(client):
def test_invalid_save_without_version_parameter(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
client.login(user)
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py
index b16bcea3..ddcf9e34 100644
--- a/tests/integration/test_permissions.py
+++ b/tests/integration/test_permissions.py
@@ -64,11 +64,11 @@ def test_owner_member_get_user_project_permissions():
project.anon_permissions = ["test1"]
project.public_permissions = ["test2"]
role = factories.RoleFactory(permissions=["test3"])
- factories.MembershipFactory(user=user1, project=project, role=role, is_owner=True)
+ factories.MembershipFactory(user=user1, project=project, role=role, is_admin=True)
expected_perms = set(
["test1", "test2", "test3"] +
- [x[0] for x in permissions.OWNERS_PERMISSIONS] +
+ [x[0] for x in permissions.ADMINS_PERMISSIONS] +
[x[0] for x in permissions.MEMBERS_PERMISSIONS]
)
assert service.get_user_project_permissions(user1, project) == expected_perms
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 04b43df7..3c8f19cc 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -90,7 +90,7 @@ def test_create_public_project_without_enough_public_projects_slots(client):
def test_change_project_from_private_to_public_without_enough_public_projects_slots(client):
project = f.create_project(is_private=True, owner__max_public_projects=0)
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {
@@ -106,7 +106,7 @@ def test_change_project_from_private_to_public_without_enough_public_projects_sl
def test_change_project_from_public_to_private_without_enough_private_projects_slots(client):
project = f.create_project(is_private=False, owner__max_private_projects=0)
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {
@@ -152,7 +152,7 @@ def test_create_public_project_with_enough_public_projects_slots(client):
def test_change_project_from_private_to_public_with_enough_public_projects_slots(client):
project = f.create_project(is_private=True, owner__max_public_projects=1)
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {
@@ -167,7 +167,7 @@ def test_change_project_from_private_to_public_with_enough_public_projects_slots
def test_change_project_from_public_to_private_with_enough_private_projects_slots(client):
project = f.create_project(is_private=False, owner__max_private_projects=1)
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {
@@ -182,7 +182,7 @@ def test_change_project_from_public_to_private_with_enough_private_projects_slot
def test_change_project_other_data_with_enough_private_projects_slots(client):
project = f.create_project(is_private=True, owner__max_private_projects=1)
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {
@@ -197,7 +197,7 @@ def test_change_project_other_data_with_enough_private_projects_slots(client):
def test_partially_update_project(client):
project = f.create_project()
- f.MembershipFactory(user=project.owner, project=project, is_owner=True)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {"name": ""}
@@ -248,7 +248,7 @@ def test_task_status_is_closed_changed_recalc_us_is_closed(client):
def test_us_status_slug_generation(client):
us_status = f.UserStoryStatusFactory(name="NEW")
- f.MembershipFactory(user=us_status.project.owner, project=us_status.project, is_owner=True)
+ f.MembershipFactory(user=us_status.project.owner, project=us_status.project, is_admin=True)
assert us_status.slug == "new"
client.login(us_status.project.owner)
@@ -268,7 +268,7 @@ def test_us_status_slug_generation(client):
def test_task_status_slug_generation(client):
task_status = f.TaskStatusFactory(name="NEW")
- f.MembershipFactory(user=task_status.project.owner, project=task_status.project, is_owner=True)
+ f.MembershipFactory(user=task_status.project.owner, project=task_status.project, is_admin=True)
assert task_status.slug == "new"
client.login(task_status.project.owner)
@@ -288,7 +288,7 @@ def test_task_status_slug_generation(client):
def test_issue_status_slug_generation(client):
issue_status = f.IssueStatusFactory(name="NEW")
- f.MembershipFactory(user=issue_status.project.owner, project=issue_status.project, is_owner=True)
+ f.MembershipFactory(user=issue_status.project.owner, project=issue_status.project, is_admin=True)
assert issue_status.slug == "new"
client.login(issue_status.project.owner)
@@ -309,7 +309,7 @@ def test_issue_status_slug_generation(client):
def test_points_name_duplicated(client):
point_1 = f.PointsFactory()
point_2 = f.PointsFactory(project=point_1.project)
- f.MembershipFactory(user=point_1.project.owner, project=point_1.project, is_owner=True)
+ f.MembershipFactory(user=point_1.project.owner, project=point_1.project, is_admin=True)
client.login(point_1.project.owner)
url = reverse("points-detail", kwargs={"pk": point_2.pk})
@@ -396,7 +396,7 @@ def test_leave_project_valid_membership_only_owner(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project, permissions=["view_project"])
- f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
client.login(user)
url = reverse("projects-leave", args=(project.id,))
response = client.post(url)
@@ -409,8 +409,8 @@ def test_leave_project_valid_membership_real_owner(client):
member_user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=owner_user)
role = f.RoleFactory.create(project=project, permissions=["view_project"])
- f.MembershipFactory.create(project=project, user=owner_user, role=role, is_owner=True)
- f.MembershipFactory.create(project=project, user=member_user, role=role, is_owner=True)
+ f.MembershipFactory.create(project=project, user=owner_user, role=role, is_admin=True)
+ f.MembershipFactory.create(project=project, user=member_user, role=role, is_admin=True)
client.login(owner_user)
url = reverse("projects-leave", args=(project.id,))
@@ -448,7 +448,7 @@ def test_delete_membership_only_owner(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project, permissions=["view_project"])
- membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
client.login(user)
url = reverse("memberships-detail", args=(membership.id,))
response = client.delete(url)
@@ -461,8 +461,8 @@ def test_delete_membership_real_owner(client):
member_user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=owner_user)
role = f.RoleFactory.create(project=project, permissions=["view_project"])
- owner_membership = f.MembershipFactory.create(project=project, user=owner_user, role=role, is_owner=True)
- f.MembershipFactory.create(project=project, user=member_user, role=role, is_owner=True)
+ owner_membership = f.MembershipFactory.create(project=project, user=owner_user, role=role, is_admin=True)
+ f.MembershipFactory.create(project=project, user=member_user, role=role, is_admin=True)
client.login(owner_user)
url = reverse("memberships-detail", args=(owner_membership.id,))
@@ -475,22 +475,22 @@ def test_edit_membership_only_owner(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project, permissions=["view_project"])
- membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
data = {
- "is_owner": False
+ "is_admin": False
}
client.login(user)
url = reverse("memberships-detail", args=(membership.id,))
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
- assert response.data["is_owner"][0] == "The project must have an owner and at least one of the users must be an active admin"
+ assert response.data["is_admin"][0] == "In this project at least one of the users must be an active admin."
def test_anon_permissions_generation_when_making_project_public(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(is_private=True)
role = f.RoleFactory.create(project=project, permissions=["view_project", "modify_project"])
- membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
assert project.anon_permissions == []
client.login(user)
url = reverse("projects-detail", kwargs={"pk": project.pk})
@@ -504,7 +504,7 @@ def test_anon_permissions_generation_when_making_project_public(client):
def test_destroy_point_and_reassign(client):
project = f.ProjectFactory.create()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
p1 = f.PointsFactory(project=project)
project.default_points = p1
project.save()
@@ -549,7 +549,7 @@ def test_create_and_use_template(client):
user = f.UserFactory.create(is_superuser=True)
project = f.create_project()
role = f.RoleFactory(project=project)
- f.MembershipFactory(user=user, project=project, is_owner=True, role=role)
+ f.MembershipFactory(user=user, project=project, is_admin=True, role=role)
client.login(user)
url = reverse("projects-create-template", kwargs={"pk": project.pk})
@@ -575,11 +575,11 @@ def test_projects_user_order(client):
user = f.UserFactory.create(is_superuser=True)
project_1 = f.create_project()
role_1 = f.RoleFactory(project=project_1)
- f.MembershipFactory(user=user, project=project_1, is_owner=True, role=role_1, user_order=2)
+ f.MembershipFactory(user=user, project=project_1, is_admin=True, role=role_1, user_order=2)
project_2 = f.create_project()
role_2 = f.RoleFactory(project=project_2)
- f.MembershipFactory(user=user, project=project_2, is_owner=True, role=role_2, user_order=1)
+ f.MembershipFactory(user=user, project=project_2, is_admin=True, role=role_2, user_order=1)
client.login(user)
#Testing default id order
@@ -762,7 +762,7 @@ def test_transfer_request_from_not_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
- f.MembershipFactory(user=user, project=project, role=role, is_owner=False)
+ f.MembershipFactory(user=user, project=project, role=role, is_admin=False)
url = reverse("projects-transfer-request", args=(project.id,))
@@ -778,7 +778,7 @@ def test_transfer_request_from_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
- f.MembershipFactory(user=user, project=project, role=role, is_owner=True)
+ f.MembershipFactory(user=user, project=project, role=role, is_admin=True)
url = reverse("projects-transfer-request", args=(project.id,))
@@ -792,7 +792,7 @@ def test_transfer_request_from_admin_member(client):
def test_project_transfer_start_to_not_a_membership(client):
user_from = f.UserFactory.create()
project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
@@ -809,7 +809,7 @@ def test_project_transfer_start_to_not_a_membership_admin(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
f.MembershipFactory(user=user_to, project=project)
client.login(user_from)
@@ -827,8 +827,8 @@ def test_project_transfer_start_to_a_valid_user(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
@@ -854,8 +854,8 @@ def test_project_transfer_reject_from_admin_member_without_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -877,8 +877,8 @@ def test_project_transfer_reject_from_not_admin_member(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=False)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=False)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -900,8 +900,8 @@ def test_project_transfer_reject_from_admin_member_with_invalid_token(client):
project = f.create_project(owner=user_from, transfer_token="invalid-token")
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -927,8 +927,8 @@ def test_project_transfer_reject_from_admin_member_with_other_user_token(client)
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -953,8 +953,8 @@ def test_project_transfer_reject_from_admin_member_with_expired_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -979,8 +979,8 @@ def test_project_transfer_reject_from_admin_member_with_valid_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -1005,8 +1005,8 @@ def test_project_transfer_accept_from_admin_member_without_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1028,8 +1028,8 @@ def test_project_transfer_accept_from_not_admin_member(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=False)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=False)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1051,8 +1051,8 @@ def test_project_transfer_accept_from_admin_member_with_invalid_token(client):
project = f.create_project(owner=user_from, transfer_token="invalid-token")
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1078,8 +1078,8 @@ def test_project_transfer_accept_from_admin_member_with_other_user_token(client)
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1104,8 +1104,8 @@ def test_project_transfer_accept_from_admin_member_with_expired_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1130,8 +1130,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1158,8 +1158,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=False)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
@@ -1192,8 +1192,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
@@ -1226,8 +1226,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_owner=True)
- f.MembershipFactory(user=user_to, project=project, is_owner=True)
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
diff --git a/tests/integration/test_references_sequences.py b/tests/integration/test_references_sequences.py
index f1a96e9a..6c856037 100644
--- a/tests/integration/test_references_sequences.py
+++ b/tests/integration/test_references_sequences.py
@@ -152,7 +152,7 @@ def test_params_validation_in_api_request(client, refmodels):
project = factories.ProjectFactory.create(owner=user)
seqname1 = refmodels.make_sequence_name(project)
role = factories.RoleFactory.create(project=project)
- factories.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
+ factories.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
milestone = factories.MilestoneFactory.create(project=project)
us = factories.UserStoryFactory.create(project=project)
diff --git a/tests/integration/test_roles.py b/tests/integration/test_roles.py
index 0a39b53e..7dc978f5 100644
--- a/tests/integration/test_roles.py
+++ b/tests/integration/test_roles.py
@@ -36,7 +36,7 @@ def test_destroy_role_and_reassign_members(client):
project = f.ProjectFactory.create(owner=user1)
role1 = f.RoleFactory.create(project=project)
role2 = f.RoleFactory.create(project=project)
- f.MembershipFactory.create(project=project, user=user1, role=role1, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project, user=user2, role=role2)
url = reverse("roles-detail", args=[role2.pk]) + "?moveTo={}".format(role1.pk)
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index 2fb2fc78..2e8203fd 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -43,7 +43,7 @@ def test_create_task_without_status(client):
project.default_task_status = status
project.save()
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("tasks-list")
data = {"subject": "Test user story", "project": project.id}
@@ -56,7 +56,7 @@ def test_create_task_without_status(client):
def test_create_task_without_default_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user, default_task_status=None)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("tasks-list")
data = {"subject": "Test user story", "project": project.id}
@@ -69,7 +69,7 @@ def test_create_task_without_default_values(client):
def test_api_update_task_tags(client):
project = f.ProjectFactory.create()
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
- f.MembershipFactory.create(project=project, user=task.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=task.owner, is_admin=True)
url = reverse("tasks-detail", kwargs={"pk": task.pk})
data = {"tags": ["back", "front"], "version": task.version}
@@ -81,7 +81,7 @@ def test_api_update_task_tags(client):
def test_api_create_in_bulk_with_status(client):
us = f.create_userstory()
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
us.project.default_task_status = f.TaskStatusFactory.create(project=us.project)
url = reverse("tasks-bulk-create")
data = {
@@ -104,7 +104,7 @@ def test_api_create_invalid_task(client):
# But the User Story is not associated with the milestone
us_milestone = f.MilestoneFactory.create()
us = f.create_userstory(milestone=us_milestone)
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
us.project.default_task_status = f.TaskStatusFactory.create(project=us.project)
task_milestone = f.MilestoneFactory.create(project=us.project, owner=us.owner)
@@ -124,7 +124,7 @@ def test_api_create_invalid_task(client):
def test_api_update_order_in_bulk(client):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
task1 = f.create_task(project=project)
task2 = f.create_task(project=project)
diff --git a/tests/integration/test_throwttling.py b/tests/integration/test_throwttling.py
index d3c049ed..e231188f 100644
--- a/tests/integration/test_throwttling.py
+++ b/tests/integration/test_throwttling.py
@@ -55,7 +55,7 @@ def test_anonimous_throttling_policy(client, settings):
def test_user_throttling_policy(client, settings):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
client.login(project.owner)
@@ -84,7 +84,7 @@ def test_user_throttling_policy(client, settings):
def test_import_mode_throttling_policy(client, settings):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py
index eb4b2803..c9bc14b3 100644
--- a/tests/integration/test_totals_projects.py
+++ b/tests/integration/test_totals_projects.py
@@ -112,7 +112,7 @@ def test_project_totals_updated_on_activity(client):
def test_project_totals_updated_on_like(client):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
totals_updated_datetime = project.totals_updated_datetime
now = datetime.datetime.now()
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index c50a9710..75b3098a 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -50,8 +50,8 @@ def test_create_userstory_with_watchers(client):
user = f.UserFactory.create()
user_watcher = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
- f.MembershipFactory.create(project=project, user=user_watcher, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
+ f.MembershipFactory.create(project=project, user=user_watcher, is_admin=True)
url = reverse("userstories-list")
data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]}
@@ -69,7 +69,7 @@ def test_create_userstory_without_status(client):
project.default_us_status = status
project.save()
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("userstories-list")
data = {"subject": "Test user story", "project": project.id}
@@ -82,7 +82,7 @@ def test_create_userstory_without_status(client):
def test_create_userstory_without_default_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user, default_us_status=None)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("userstories-list")
data = {"subject": "Test user story", "project": project.id}
@@ -94,7 +94,7 @@ def test_create_userstory_without_default_values(client):
def test_api_delete_userstory(client):
us = f.UserStoryFactory.create()
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
url = reverse("userstories-detail", kwargs={"pk": us.pk})
client.login(us.owner)
@@ -106,7 +106,7 @@ def test_api_delete_userstory(client):
def test_api_filter_by_subject_or_ref(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.UserStoryFactory.create(project=project)
f.UserStoryFactory.create(project=project, subject="some random subject")
@@ -122,7 +122,7 @@ def test_api_filter_by_subject_or_ref(client):
def test_api_create_in_bulk_with_status(client):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
url = reverse("userstories-bulk-create")
data = {
"bulk_stories": "Story #1\nStory #2",
@@ -139,7 +139,7 @@ def test_api_create_in_bulk_with_status(client):
def test_api_update_orders_in_bulk(client):
project = f.create_project()
- f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
+ f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
us1 = f.create_userstory(project=project)
us2 = f.create_userstory(project=project)
@@ -172,7 +172,7 @@ def test_update_userstory_points(client):
role1 = f.RoleFactory.create(project=project)
role2 = f.RoleFactory.create(project=project)
- f.MembershipFactory.create(project=project, user=user1, role=role1, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project, user=user2, role=role2)
f.PointsFactory.create(project=project, value=None)
@@ -236,7 +236,7 @@ def test_update_userstory_rolepoints_on_add_new_role(client):
def test_archived_filter(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.UserStoryFactory.create(project=project)
archived_status = f.UserStoryStatusFactory.create(is_archived=True)
f.UserStoryFactory.create(status=archived_status, project=project)
@@ -261,7 +261,7 @@ def test_archived_filter(client):
def test_filter_by_multiple_status(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.UserStoryFactory.create(project=project)
us1 = f.UserStoryFactory.create(project=project)
us2 = f.UserStoryFactory.create(project=project)
@@ -479,7 +479,7 @@ def test_update_userstory_respecting_watchers(client):
project = f.ProjectFactory.create()
us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project)
us.add_watcher(watching_user)
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
f.MembershipFactory.create(project=us.project, user=watching_user)
client.login(user=us.owner)
@@ -496,7 +496,7 @@ def test_update_userstory_update_watchers(client):
watching_user = f.create_user()
project = f.ProjectFactory.create()
us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project)
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
f.MembershipFactory.create(project=us.project, user=watching_user)
client.login(user=us.owner)
@@ -515,7 +515,7 @@ def test_update_userstory_remove_watchers(client):
project = f.ProjectFactory.create()
us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project)
us.add_watcher(watching_user)
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
f.MembershipFactory.create(project=us.project, user=watching_user)
client.login(user=us.owner)
@@ -532,7 +532,7 @@ def test_update_userstory_remove_watchers(client):
def test_update_userstory_update_tribe_gig(client):
project = f.ProjectFactory.create()
us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project)
- f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True)
+ f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
url = reverse("userstories-detail", kwargs={"pk": us.pk})
data = {
diff --git a/tests/integration/test_vote_issues.py b/tests/integration/test_vote_issues.py
index 4af983ae..b6f8e925 100644
--- a/tests/integration/test_vote_issues.py
+++ b/tests/integration/test_vote_issues.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_upvote_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-upvote", args=(issue.id,))
client.login(user)
@@ -39,7 +39,7 @@ def test_upvote_issue(client):
def test_downvote_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-downvote", args=(issue.id,))
client.login(user)
@@ -51,7 +51,7 @@ def test_downvote_issue(client):
def test_list_issue_voters(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
f.VoteFactory.create(content_object=issue, user=user)
url = reverse("issue-voters-list", args=(issue.id,))
@@ -64,7 +64,7 @@ def test_list_issue_voters(client):
def test_get_issue_voter(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
vote = f.VoteFactory.create(content_object=issue, user=user)
url = reverse("issue-voters-detail", args=(issue.id, vote.user.id))
@@ -77,7 +77,7 @@ def test_get_issue_voter(client):
def test_get_issue_votes(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-detail", args=(issue.id,))
f.VotesFactory.create(content_object=issue, count=5)
@@ -92,7 +92,7 @@ def test_get_issue_votes(client):
def test_get_issue_is_voted(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
f.VotesFactory.create(content_object=issue)
url_detail = reverse("issues-detail", args=(issue.id,))
url_upvote = reverse("issues-upvote", args=(issue.id,))
diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py
index c400f3ad..ca3414e6 100644
--- a/tests/integration/test_vote_tasks.py
+++ b/tests/integration/test_vote_tasks.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_upvote_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-upvote", args=(task.id,))
client.login(user)
@@ -39,7 +39,7 @@ def test_upvote_task(client):
def test_downvote_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-downvote", args=(task.id,))
client.login(user)
@@ -51,7 +51,7 @@ def test_downvote_task(client):
def test_list_task_voters(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
f.VoteFactory.create(content_object=task, user=user)
url = reverse("task-voters-list", args=(task.id,))
@@ -65,7 +65,7 @@ def test_list_task_voters(client):
def test_get_task_voter(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
vote = f.VoteFactory.create(content_object=task, user=user)
url = reverse("task-voters-detail", args=(task.id, vote.user.id))
@@ -79,7 +79,7 @@ def test_get_task_voter(client):
def test_get_task_votes(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-detail", args=(task.id,))
f.VotesFactory.create(content_object=task, count=5)
@@ -94,7 +94,7 @@ def test_get_task_votes(client):
def test_get_task_is_voted(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
f.VotesFactory.create(content_object=task)
url_detail = reverse("tasks-detail", args=(task.id,))
url_upvote = reverse("tasks-upvote", args=(task.id,))
diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py
index 4fbc0e6b..b8caa01b 100644
--- a/tests/integration/test_vote_userstories.py
+++ b/tests/integration/test_vote_userstories.py
@@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db
def test_upvote_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-upvote", args=(user_story.id,))
client.login(user)
@@ -39,7 +39,7 @@ def test_upvote_user_story(client):
def test_downvote_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-downvote", args=(user_story.id,))
client.login(user)
@@ -51,7 +51,7 @@ def test_downvote_user_story(client):
def test_list_user_story_voters(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
f.VoteFactory.create(content_object=user_story, user=user)
url = reverse("userstory-voters-list", args=(user_story.id,))
@@ -64,7 +64,7 @@ def test_list_user_story_voters(client):
def test_get_userstory_voter(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
vote = f.VoteFactory.create(content_object=user_story, user=user)
url = reverse("userstory-voters-detail", args=(user_story.id, vote.user.id))
@@ -78,7 +78,7 @@ def test_get_userstory_voter(client):
def test_get_user_story_votes(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-detail", args=(user_story.id,))
f.VotesFactory.create(content_object=user_story, count=5)
@@ -93,7 +93,7 @@ def test_get_user_story_votes(client):
def test_get_user_story_is_voted(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
f.VotesFactory.create(content_object=user_story)
url_detail = reverse("userstories-detail", args=(user_story.id,))
url_upvote = reverse("userstories-upvote", args=(user_story.id,))
diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py
index 885e0a96..fc22f32c 100644
--- a/tests/integration/test_watch_issues.py
+++ b/tests/integration/test_watch_issues.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db
def test_watch_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-watch", args=(issue.id,))
client.login(user)
@@ -40,7 +40,7 @@ def test_watch_issue(client):
def test_unwatch_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-watch", args=(issue.id,))
client.login(user)
@@ -52,7 +52,7 @@ def test_unwatch_issue(client):
def test_list_issue_watchers(client):
user = f.UserFactory.create()
issue = f.IssueFactory(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=issue, user=user)
url = reverse("issue-watchers-list", args=(issue.id,))
@@ -66,7 +66,7 @@ def test_list_issue_watchers(client):
def test_get_issue_watcher(client):
user = f.UserFactory.create()
issue = f.IssueFactory(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=issue, user=user)
url = reverse("issue-watchers-detail", args=(issue.id, watch.user.id))
@@ -80,7 +80,7 @@ def test_get_issue_watcher(client):
def test_get_issue_watchers(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url = reverse("issues-detail", args=(issue.id,))
f.WatchedFactory.create(content_object=issue, user=user)
@@ -96,7 +96,7 @@ def test_get_issue_watchers(client):
def test_get_issue_is_watcher(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=issue.project, user=user, is_admin=True)
url_detail = reverse("issues-detail", args=(issue.id,))
url_watch = reverse("issues-watch", args=(issue.id,))
url_unwatch = reverse("issues-unwatch", args=(issue.id,))
diff --git a/tests/integration/test_watch_milestones.py b/tests/integration/test_watch_milestones.py
index dcb21524..da17f408 100644
--- a/tests/integration/test_watch_milestones.py
+++ b/tests/integration/test_watch_milestones.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db
def test_watch_milestone(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
url = reverse("milestones-watch", args=(milestone.id,))
client.login(user)
@@ -40,7 +40,7 @@ def test_watch_milestone(client):
def test_unwatch_milestone(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
url = reverse("milestones-watch", args=(milestone.id,))
client.login(user)
@@ -52,7 +52,7 @@ def test_unwatch_milestone(client):
def test_list_milestone_watchers(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=milestone, user=user)
url = reverse("milestone-watchers-list", args=(milestone.id,))
@@ -66,7 +66,7 @@ def test_list_milestone_watchers(client):
def test_get_milestone_watcher(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=milestone, user=user)
url = reverse("milestone-watchers-detail", args=(milestone.id, watch.user.id))
@@ -80,7 +80,7 @@ def test_get_milestone_watcher(client):
def test_get_milestone_watchers(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
url = reverse("milestones-detail", args=(milestone.id,))
f.WatchedFactory.create(content_object=milestone, user=user)
@@ -95,7 +95,7 @@ def test_get_milestone_watchers(client):
def test_get_milestone_is_watcher(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
- f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True)
url_detail = reverse("milestones-detail", args=(milestone.id,))
url_watch = reverse("milestones-watch", args=(milestone.id,))
url_unwatch = reverse("milestones-unwatch", args=(milestone.id,))
diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py
index f49d9160..2608864b 100644
--- a/tests/integration/test_watch_projects.py
+++ b/tests/integration/test_watch_projects.py
@@ -30,7 +30,7 @@ pytestmark = pytest.mark.django_db
def test_watch_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-watch", args=(project.id,))
client.login(user)
@@ -42,7 +42,7 @@ def test_watch_project(client):
def test_watch_project_with_valid_notify_level(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-watch", args=(project.id,))
client.login(user)
@@ -57,7 +57,7 @@ def test_watch_project_with_valid_notify_level(client):
def test_watch_project_with_invalid_notify_level(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-watch", args=(project.id,))
client.login(user)
@@ -73,7 +73,7 @@ def test_watch_project_with_invalid_notify_level(client):
def test_unwatch_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-unwatch", args=(project.id,))
client.login(user)
@@ -85,7 +85,7 @@ def test_unwatch_project(client):
def test_list_project_watchers(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=project, user=user)
url = reverse("project-watchers-list", args=(project.id,))
@@ -99,7 +99,7 @@ def test_list_project_watchers(client):
def test_get_project_watcher(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=project, user=user)
url = reverse("project-watchers-detail", args=(project.id, watch.user.id))
@@ -113,7 +113,7 @@ def test_get_project_watcher(client):
def test_get_project_watchers(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
- f.MembershipFactory.create(project=project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
url = reverse("projects-detail", args=(project.id,))
f.WatchedFactory.create(content_object=project, user=user)
diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py
index c0fbc59b..8c716560 100644
--- a/tests/integration/test_watch_tasks.py
+++ b/tests/integration/test_watch_tasks.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db
def test_watch_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-watch", args=(task.id,))
client.login(user)
@@ -40,7 +40,7 @@ def test_watch_task(client):
def test_unwatch_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-watch", args=(task.id,))
client.login(user)
@@ -52,7 +52,7 @@ def test_unwatch_task(client):
def test_list_task_watchers(client):
user = f.UserFactory.create()
task = f.TaskFactory(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=task, user=user)
url = reverse("task-watchers-list", args=(task.id,))
@@ -66,7 +66,7 @@ def test_list_task_watchers(client):
def test_get_task_watcher(client):
user = f.UserFactory.create()
task = f.TaskFactory(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=task, user=user)
url = reverse("task-watchers-detail", args=(task.id, watch.user.id))
@@ -80,7 +80,7 @@ def test_get_task_watcher(client):
def test_get_task_watchers(client):
user = f.UserFactory.create()
task = f.TaskFactory(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-detail", args=(task.id,))
f.WatchedFactory.create(content_object=task, user=user)
@@ -96,7 +96,7 @@ def test_get_task_watchers(client):
def test_get_task_is_watcher(client):
user = f.UserFactory.create()
task = f.create_task(owner=user, milestone=None)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url_detail = reverse("tasks-detail", args=(task.id,))
url_watch = reverse("tasks-watch", args=(task.id,))
url_unwatch = reverse("tasks-unwatch", args=(task.id,))
diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py
index 7e940664..66ae4e0c 100644
--- a/tests/integration/test_watch_userstories.py
+++ b/tests/integration/test_watch_userstories.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db
def test_watch_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-watch", args=(user_story.id,))
client.login(user)
@@ -40,7 +40,7 @@ def test_watch_user_story(client):
def test_unwatch_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-unwatch", args=(user_story.id,))
client.login(user)
@@ -52,7 +52,7 @@ def test_unwatch_user_story(client):
def test_list_user_story_watchers(client):
user = f.UserFactory.create()
user_story = f.UserStoryFactory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=user_story, user=user)
url = reverse("userstory-watchers-list", args=(user_story.id,))
@@ -66,7 +66,7 @@ def test_list_user_story_watchers(client):
def test_get_user_story_watcher(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=user_story, user=user)
url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id))
@@ -80,7 +80,7 @@ def test_get_user_story_watcher(client):
def test_get_user_story_watchers(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-detail", args=(user_story.id,))
f.WatchedFactory.create(content_object=user_story, user=user)
@@ -96,7 +96,7 @@ def test_get_user_story_watchers(client):
def test_get_user_story_is_watcher(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user, status=None)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url_detail = reverse("userstories-detail", args=(user_story.id,))
url_watch = reverse("userstories-watch", args=(user_story.id,))
url_unwatch = reverse("userstories-unwatch", args=(user_story.id,))
diff --git a/tests/integration/test_watch_wikipages.py b/tests/integration/test_watch_wikipages.py
index 510c7015..6940368d 100644
--- a/tests/integration/test_watch_wikipages.py
+++ b/tests/integration/test_watch_wikipages.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db
def test_watch_wikipage(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
url = reverse("wiki-watch", args=(wikipage.id,))
client.login(user)
@@ -40,7 +40,7 @@ def test_watch_wikipage(client):
def test_unwatch_wikipage(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
url = reverse("wiki-watch", args=(wikipage.id,))
client.login(user)
@@ -52,7 +52,7 @@ def test_unwatch_wikipage(client):
def test_list_wikipage_watchers(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
f.WatchedFactory.create(content_object=wikipage, user=user)
url = reverse("wiki-watchers-list", args=(wikipage.id,))
@@ -66,7 +66,7 @@ def test_list_wikipage_watchers(client):
def test_get_wikipage_watcher(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
watch = f.WatchedFactory.create(content_object=wikipage, user=user)
url = reverse("wiki-watchers-detail", args=(wikipage.id, watch.user.id))
@@ -80,7 +80,7 @@ def test_get_wikipage_watcher(client):
def test_get_wikipage_watchers(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
url = reverse("wiki-detail", args=(wikipage.id,))
f.WatchedFactory.create(content_object=wikipage, user=user)
@@ -95,7 +95,7 @@ def test_get_wikipage_watchers(client):
def test_get_wikipage_is_watcher(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
- f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
+ f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True)
url_detail = reverse("wiki-detail", args=(wikipage.id,))
url_watch = reverse("wiki-watch", args=(wikipage.id,))
url_unwatch = reverse("wiki-unwatch", args=(wikipage.id,))
From 186eab2610c971aba291f5cfdb7b2ccca213ab8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 19 Feb 2016 17:25:20 +0100
Subject: [PATCH 046/105] [i18n] Update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 715 ++++++++++++++------
taiga/locale/de/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/en/LC_MESSAGES/django.po | 707 ++++++++++++++------
taiga/locale/es/LC_MESSAGES/django.po | 712 ++++++++++++++------
taiga/locale/fi/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/fr/LC_MESSAGES/django.po | 731 ++++++++++++++------
taiga/locale/it/LC_MESSAGES/django.po | 715 ++++++++++++++------
taiga/locale/nl/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/pl/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/ru/LC_MESSAGES/django.po | 741 +++++++++++++++------
taiga/locale/sv/LC_MESSAGES/django.po | 713 ++++++++++++++------
taiga/locale/tr/LC_MESSAGES/django.po | 715 ++++++++++++++------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 715 ++++++++++++++------
14 files changed, 7136 insertions(+), 2893 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 27f087a7..4bdb1457 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -66,7 +66,8 @@ msgid "Error on creating new user."
msgstr "Error creant un nou usuari."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Token invàlid"
@@ -182,6 +183,15 @@ msgstr ""
"Puja una imatge vàlida. El fitxer que has pujat no ès una imatge o el fitxer "
"està corrupte."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La página no es 'last' ni pot ser convertida a un 'int'"
@@ -239,23 +249,23 @@ msgstr ""
msgid "No input provided"
msgstr ""
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr ""
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr ""
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr ""
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr ""
@@ -330,12 +340,12 @@ msgstr "Error d'integritat per argument invàlid o erroni."
msgid "Precondition error"
msgstr "Precondició errònia."
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr ""
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr ""
@@ -454,71 +464,71 @@ msgstr ""
" Comentari: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Es necessita arxiu dump."
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Format d'arxiu dump invàlid"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr ""
@@ -537,9 +547,7 @@ msgid "It contain invalid custom fields."
msgstr "Conté camps personalitzats invàlids."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -705,11 +713,11 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nom"
@@ -722,11 +730,11 @@ msgstr ""
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Descripció"
@@ -740,7 +748,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr ""
@@ -748,11 +756,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "Nom complet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "Adreça d'email"
@@ -760,11 +768,11 @@ msgstr "Adreça d'email"
msgid "comment"
msgstr "Comentari"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -836,8 +844,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "El payload no és un arxiu json vàlid"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El projecte no existeix"
@@ -1135,95 +1143,109 @@ msgstr "Administrar valors de projecte"
msgid "Admin roles"
msgstr "Administrar rols"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Arguments incomplets."
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Format d'image invàlid"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Al menys un del usuaris ha de ser administrador"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "Amo"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "Projecte"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "Tipus de contingut"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "Id d'objecte"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "Data de modificació"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "Arxiu adjunt"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "està obsolet "
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "Ordre"
@@ -1244,18 +1266,30 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
+msgid "Multi-Line Text"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr ""
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1415,23 +1449,23 @@ msgstr "nota de bloqueig"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tens permissos per a ficar aquest sprint a aquesta incidència"
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "No tens permissos per a ficar aquest status a aquesta tasca"
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tens permissos per a ficar aquesta severitat a aquesta tasca"
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tens permissos per a ficar aquesta prioritat a aquesta incidència"
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "No tens permissos per a ficar aquest tipus a aquesta incidència"
@@ -1483,11 +1517,11 @@ msgstr "M'agrada"
#: taiga/projects/likes/models.py:36
msgid "Likes"
-msgstr ""
+msgstr "Fans"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1500,8 +1534,8 @@ msgstr "Data estimada d'inici"
msgid "estimated finish date"
msgstr "Data estimada de finalització"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "està tancat"
@@ -1530,224 +1564,232 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "text extra d'invitació"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "L'usuari ja es membre del projecte"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "Points per defecte"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "estatus d'història d'usuai per defecte"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "Estatus de tasca per defecte"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "Prioritat per defecte"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "Severitat per defecte"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "Status d'incidència per defecte"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "Tipus d'incidència per defecte"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de fites"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "total de punts d'història"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "activa panell de backlog"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "activa panell de kanban"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "activa panell de wiki"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "activa panell d'incidències"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de videoconferència"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "template de creació"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "permisos d'anònims"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "permisos d'usuaris"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "es privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "colors de tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Actualitzada data"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configuració de mòdules"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "està arxivat"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "color"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "limit de treball en progrés"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "rol d'amo per defecte"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "opcions per defecte"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "status d'històries d'usuari"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punts"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "status de tasques"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "status d'incidències"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "tipus d'incidències"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "prioritats"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "severitats"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "rols"
@@ -1763,20 +1805,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "creada data"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr ""
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr ""
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr ""
@@ -2287,50 +2329,51 @@ msgid "version"
msgstr "Versió"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "No pots deixar el projecte si no hi ha més amos"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Aquest e-mail ja està en ús"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Rol invàlid per al projecte"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Rols"
@@ -2342,15 +2385,26 @@ msgstr ""
msgid "Project End"
msgstr ""
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Token invàlid"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr ""
@@ -2496,6 +2550,223 @@ msgstr ""
"\n"
"[Taiga] Afegit al projecte '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2728,15 +2999,15 @@ msgstr ""
msgid "Stakeholder"
msgstr ""
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -2795,11 +3066,11 @@ msgstr "Vots"
msgid "Vote"
msgstr "Vot"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr ""
@@ -2819,65 +3090,65 @@ msgstr ""
msgid "Personal info"
msgstr "Informació personal"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Permissos"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Dates importants"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Email duplicat"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Email no vàlid"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Nom d'usuari o email invàlid"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Correu enviat satisfactòriament"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Token invàlid"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Paràmetre de password actual requerit"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Paràmetre de password requerit"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Password invàlid, al menys 6 caràcters requerits"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Password actual invàlid"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invàlid. Estás segur que el token es correcte i que no l'has usat abans?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Invàlid. Estás segur que el token es correcte?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "estatus de superusuari"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2885,24 +3156,24 @@ msgstr ""
"Designa que aquest usuari te tots els permisos sense asignarli-los "
"explícitament."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "mot d'usuari"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Requerit. 30 caràcters o menys. Lletres, nombres i caràcters /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Introdueix un nom d'usuari vàlid"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "actiu"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -2910,42 +3181,58 @@ msgstr ""
"Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en "
"lloc de borrar el compte."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "data d'unió"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "llenguatge per defecte"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "zona horaria per defecte"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "coloritza tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "token de correu"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nova adreça de correu"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "permissos"
@@ -2962,6 +3249,22 @@ msgstr "Nom d'usuari invàlid"
msgid "Username or password does not matches user."
msgstr ""
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 19f5adfd..2b98be80 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -76,7 +76,8 @@ msgid "Error on creating new user."
msgstr "Fehler bei der Erstellung des neuen Benutzers."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Ungültiges Token"
@@ -212,6 +213,15 @@ msgstr ""
"Bitte laden Sie ein gültiges Bild hoch. Die Datei, die Sie hochgeladen "
"haben, ist entweder kein Bild oder defekt."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Seite ist nicht 'letzte', noch kann diese konvertiert werden."
@@ -269,25 +279,25 @@ msgstr "Ungültige Daten"
msgid "No input provided"
msgstr "Es gab keine Eingabe"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Es können nur existierende Einträge aktualisiert werden. Eine Neuerstellung "
"ist nicht möglich."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Es wurde eine Liste von Einträgen erwartet."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Nicht gefunden."
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Zugriff verweigert"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Fehler bei der Serveranmeldung"
@@ -362,12 +372,12 @@ msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente"
msgid "Precondition error"
msgstr "Voraussetzungsfehler"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Fehler in Filter Parameter Typen."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
@@ -509,71 +519,71 @@ msgstr ""
"Kommentar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Es ist mindestens eine Rolle nötig"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Exportdatei erforderlich"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Ungültiges Exportdatei Format"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "Fehler beim Importieren der Projektdaten"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "Fehler beim Importieren der Listen von Projektattributen"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte "
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "Fehler beim Importieren der Kundenattribute"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "Fehler beim Importieren der Rollen"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "Fehler beim Importieren der Mitgliedschaften"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "Fehler beim Import der Sprints"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "Fehler beim Importieren von Wiki Seiten"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "Fehler beim Importieren von Wiki Links"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "Fehler beim Importieren der Tickets"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "Fehler beim Importieren der User-Stories"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "Fehler beim Importieren der Aufgaben"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "Fehler beim Importieren der Schlagworte"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
@@ -592,9 +602,7 @@ msgid "It contain invalid custom fields."
msgstr "Enthält ungültige Benutzerfelder."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Der Name für das Projekt ist doppelt vergeben"
@@ -853,11 +861,11 @@ msgstr "Authentifizierung erforderlich"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "Name"
@@ -870,11 +878,11 @@ msgstr "Icon URL"
msgid "web"
msgstr "Web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Beschreibung"
@@ -888,7 +896,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "Geheimer Schlüssel für Verschlüsselung der Anwensungs-Token"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "Benutzer"
@@ -896,11 +904,11 @@ msgstr "Benutzer"
msgid "application"
msgstr "Applikation"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "vollständiger Name"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "E-Mail Adresse"
@@ -908,11 +916,11 @@ msgstr "E-Mail Adresse"
msgid "comment"
msgstr "Kommentar"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -984,8 +992,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Die Nutzlast ist kein gültiges json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Das Projekt existiert nicht"
@@ -1325,95 +1333,109 @@ msgstr "Administrator Projekt Werte"
msgid "Admin roles"
msgstr "Administrator-Rollen"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Unvollständige Argumente"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ungültiges Bildformat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Unglültiger Templatename"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Ungültige Templatebeschreibung"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Mindestens ein Benutzer muss ein aktiver Administrator sein. "
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Teil-Aktualisierungen sind nicht unterstützt"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "Besitzer"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "Projekt"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "Inhaltsart"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "Objekt Nr."
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "Zeitpunkt der Änderung"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "Angehangene Datei"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "SHA1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "wurde verworfen"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "Reihenfolge"
@@ -1434,18 +1456,30 @@ msgstr "Kunde"
msgid "Talky"
msgstr "Gesprächig"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Text"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Mehrzeiliger Text"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Datum"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1605,27 +1639,27 @@ msgstr "Blockierungsgrund"
msgid "sprint"
msgstr "Sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Sprint zu setzen."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Status zu setzen. "
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Gewichtung zu setzen."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Priorität zu setzen. "
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Sie haben nicht die Berechtigung, das Ticket auf diese Art zu setzen."
@@ -1679,9 +1713,9 @@ msgstr "Like"
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "Slug"
@@ -1694,8 +1728,8 @@ msgstr "geschätzter Starttermin"
msgid "estimated finish date"
msgstr "geschätzter Endtermin"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "ist geschlossen"
@@ -1724,224 +1758,232 @@ msgstr "'{param}' Parameter ist ein Pflichtfeld"
msgid "'project' parameter is mandatory"
msgstr "Der 'project' Parameter ist ein Pflichtfeld"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "E-Mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "erstellt am "
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "Token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "Einladung Zusatztext "
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "Benutzerreihenfolge"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Der Benutzer ist bereits Mitglied dieses Projekts"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "voreingestellte Punkte"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "voreingesteller User-Story Status "
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "voreingestellter Aufgabenstatus"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "voreingestellte Priorität "
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "voreingestellte Gewichtung "
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "voreingestellter Ticket Status"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "voreingestellter Ticket Typ"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "Mitglieder"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "Meilensteine Gesamt"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "Story Punkte insgesamt"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktives Backlog Panel"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktives Kanban Panel"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktives Wiki Panel"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktives Tickets Panel"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "Videokonferenzsystem"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "Zusatzdaten Videokonferenz"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "Vorlage erstellen"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "Rechte für anonyme Nutzer"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "Rechte für registrierte Nutzer"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "ist privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "Tag Farben"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Aktualisierungsdatum"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "Count"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "Module konfigurieren"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "ist archiviert"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "Farbe"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "Ausführungslimit"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "Wert"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "voreingestellte Besitzerrolle"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "Vorgabe Optionen"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "User-Story Status "
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "Punkte"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "Ticket Status"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "Ticket Arten"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "Prioritäten"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "Gewichtung"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "Rollen"
@@ -1957,20 +1999,20 @@ msgstr "Alle"
msgid "None"
msgstr "Keine"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "Erstelldatum"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "Chronik Einträge"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "Benutzer benachrichtigen"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Beobachtet"
@@ -2759,52 +2801,51 @@ msgid "version"
msgstr "Version"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-"Sie können das Projekt nicht verlassen, wenn keine weiteren Besitzer "
-"vorhanden sind"
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Die E-Mailadresse ist bereits vergeben"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Ungültige Rolle für dieses Projekt"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Rollen"
@@ -2816,18 +2857,29 @@ msgstr "Zukünftiger Sprint"
msgid "Project End"
msgstr "Projektende"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Token ist ungültig"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen"
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diese User-Story auf diese Aufgabe zu "
"setzen"
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen."
@@ -3006,6 +3058,223 @@ msgstr ""
" [Taiga] Zum Projekt hinzugefügt '%(project)s'\n"
" \n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3249,19 +3518,19 @@ msgstr "Projekteigentümer "
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Sprint auf diese User-Story zu "
"setzen."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Status auf diese User-Story zu "
"setzen."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Erstelle die User-Story #{ref} - {subject}"
@@ -3320,11 +3589,11 @@ msgstr "Stimmen"
msgid "Vote"
msgstr "Stimme"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content' Parameter ist erforderlich"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' Parameter ist erforderlich"
@@ -3344,66 +3613,66 @@ msgstr "Prüfe die API der Historie auf Übereinstimmung"
msgid "Personal info"
msgstr "Personal Information"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Berechtigungen"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Wichtige Termine"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Doppelte E-Mail"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Ungültige E-Mail"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Ungültiger Benutzername oder E-Mail"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "E-Mail erfolgreich gesendet."
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Token ist ungültig"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Aktueller Passwort Parameter wird benötigt"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Neuer Passwort Parameter benötigt"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Ungültiges aktuelles Passwort"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Ungültig. Sind Sie sicher, dass das Token korrekt ist und Sie es nicht "
"bereits verwendet haben?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "Superuser Status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3411,25 +3680,25 @@ msgstr ""
"Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor "
"zugewiesen werden müssen. "
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "Benutzername"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Benötigt. 30 Zeichen oder weniger.. Buchstaben, Zahlen und /./-/_ Zeichen"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Geben Sie einen gültigen Benuzternamen ein."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3437,42 +3706,58 @@ msgstr ""
"Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen "
"Benutzer zu löschen."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "Über mich"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "Foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "Beitrittsdatum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "Vorgegebene Sprache"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "Standard-Theme"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "Vorgegebene Zeitzone"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "Tag-Farben"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "E-Mail Token"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "neue E-Mail Adresse"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "Berechtigungen"
@@ -3489,6 +3774,22 @@ msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen."
msgid "Username or password does not matches user."
msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 7e445d3f..a08f62f6 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-01-29 10:21+0100\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -62,7 +62,8 @@ msgid "Error on creating new user."
msgstr ""
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr ""
@@ -174,6 +175,15 @@ msgid ""
"corrupted image."
msgstr ""
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
@@ -231,23 +241,23 @@ msgstr ""
msgid "No input provided"
msgstr ""
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr ""
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr ""
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr ""
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr ""
@@ -322,12 +332,12 @@ msgstr ""
msgid "Precondition error"
msgstr ""
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr ""
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr ""
@@ -443,71 +453,71 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr ""
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr ""
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr ""
@@ -526,9 +536,7 @@ msgid "It contain invalid custom fields."
msgstr ""
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -694,11 +702,11 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr ""
@@ -711,11 +719,11 @@ msgstr ""
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr ""
@@ -729,7 +737,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr ""
@@ -737,11 +745,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr ""
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr ""
@@ -749,11 +757,11 @@ msgstr ""
msgid "comment"
msgstr ""
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -809,8 +817,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr ""
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr ""
@@ -1108,95 +1116,109 @@ msgstr ""
msgid "Admin roles"
msgstr ""
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr ""
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr ""
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr ""
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr ""
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr ""
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr ""
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr ""
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr ""
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr ""
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr ""
@@ -1217,18 +1239,30 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
+msgid "Multi-Line Text"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr ""
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1388,23 +1422,23 @@ msgstr ""
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr ""
@@ -1458,9 +1492,9 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr ""
@@ -1473,8 +1507,8 @@ msgstr ""
msgid "estimated finish date"
msgstr ""
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr ""
@@ -1503,224 +1537,232 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr ""
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr ""
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr ""
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr ""
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr ""
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr ""
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr ""
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr ""
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr ""
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr ""
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr ""
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr ""
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr ""
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr ""
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr ""
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr ""
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr ""
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr ""
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr ""
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr ""
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr ""
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr ""
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr ""
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr ""
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr ""
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr ""
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr ""
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr ""
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr ""
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr ""
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr ""
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr ""
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr ""
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr ""
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr ""
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr ""
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr ""
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr ""
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr ""
@@ -1736,20 +1778,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr ""
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr ""
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr ""
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr ""
@@ -2254,50 +2296,51 @@ msgid "version"
msgstr ""
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr ""
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr ""
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr ""
@@ -2309,15 +2352,26 @@ msgstr ""
msgid "Project End"
msgstr ""
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr ""
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr ""
@@ -2445,6 +2499,223 @@ msgid ""
"[Taiga] Added to the project '%(project)s'\n"
msgstr ""
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2677,15 +2948,15 @@ msgstr ""
msgid "Stakeholder"
msgstr ""
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -2744,11 +3015,11 @@ msgstr ""
msgid "Vote"
msgstr ""
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr ""
@@ -2768,128 +3039,144 @@ msgstr ""
msgid "Personal info"
msgstr ""
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr ""
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr ""
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr ""
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr ""
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr ""
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr ""
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr ""
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr ""
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr ""
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr ""
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr ""
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr ""
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr ""
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr ""
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr ""
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr ""
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr ""
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr ""
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr ""
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr ""
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr ""
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr ""
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr ""
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr ""
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr ""
@@ -2906,6 +3193,22 @@ msgstr ""
msgid "Username or password does not matches user."
msgstr ""
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index f3e064b4..8e9b8f17 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -13,8 +13,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-23 21:41+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -70,7 +70,8 @@ msgid "Error on creating new user."
msgstr "Error al crear un nuevo usuario "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Token inválido"
@@ -196,6 +197,15 @@ msgid ""
"corrupted image."
msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La página no es 'last' o no es un número."
@@ -254,25 +264,25 @@ msgstr "Datos invalidos"
msgid "No input provided"
msgstr "No se han introducido datos."
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"No se pueden crear nuevos objetos. Sólo está permitida la actualización de "
"los existentes."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Se esperaba una lista de objetos."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "No encontrado"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Permiso denegado."
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Error en la aplicación del servidor."
@@ -347,12 +357,12 @@ msgstr "Error de integridad por argumentos incorrectos o inválidos"
msgid "Precondition error"
msgstr "Error por incumplimiento de precondición"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Error en los típos de parámetros de filtrado"
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
@@ -493,71 +503,71 @@ msgstr ""
"\n"
"Comentario: %(comment)s"
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Necesitamos al menos un rol"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Se necesita el fichero con los datos exportados"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Formato de fichero de exportación inválido"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "error importando los datos del proyecto"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "error importando la listados de valores de attributos del proyecto"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
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:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "error importando los atributos personalizados"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "error importando los roles"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "error importando los miembros"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "error importando los sprints"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "error importando las páginas del wiki"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "error importando los enlaces del wiki"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "error importando las peticiones"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "error importando las historias de usuario"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "error importando las tareas"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "error importando las etiquetas"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "error importando los timelines"
@@ -576,9 +586,7 @@ msgid "It contain invalid custom fields."
msgstr "Contiene attributos personalizados inválidos."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nombre duplicado para el proyecto"
@@ -832,11 +840,11 @@ msgstr "Se requiere autenticación"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "nombre"
@@ -849,11 +857,11 @@ msgstr "URL del icono"
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descripción"
@@ -867,7 +875,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "clave secreta para cifrar los tokens de aplicación"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "usuario"
@@ -875,11 +883,11 @@ msgstr "usuario"
msgid "application"
msgstr "aplicación"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "nombre completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "dirección de email"
@@ -887,11 +895,11 @@ msgstr "dirección de email"
msgid "comment"
msgstr "comentario"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -962,8 +970,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "El payload no es un json válido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El proyecto no existe"
@@ -1302,95 +1310,109 @@ msgstr "Administrar valores de proyecto"
msgid "Admin roles"
msgstr "Administrar roles"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagen no válido"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Nombre de plantilla invalido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Descripción de plantilla invalida"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Al menos uno de los usuario debe ser un administrador."
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "La actualización parcial no está soportada."
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "Dueño"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "Proyecto"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "típo de contenido"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "id de objeto"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "fecha modificada"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "archivo adjunto"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "está desactualizado"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "orden"
@@ -1411,18 +1433,30 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Texto"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Texto multilínea"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Fecha"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1582,23 +1616,23 @@ msgstr "nota de bloqueo"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tienes permisos para asignar un sprint a esta petición."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "No tienes permisos para asignar un estado a esta petición."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tienes permisos para establecer la gravedad de esta petición."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tienes permiso para establecer la prioridad de esta petición."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "No tienes permiso para establecer el tipo de esta petición."
@@ -1652,9 +1686,9 @@ msgstr "Like"
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1667,8 +1701,8 @@ msgstr "fecha estimada de comienzo"
msgid "estimated finish date"
msgstr "fecha estimada de finalización"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "está cerrada"
@@ -1699,224 +1733,232 @@ msgstr "el parámetro '{param}' es obligatório"
msgid "'project' parameter is mandatory"
msgstr "el parámetro 'project' es obligatório"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "creado el"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "texto extra de la invitación"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "orden del usuario"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "El usuario ya es miembro del proyecto"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "puntos por defecto"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "estado de historia por defecto"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "estado de tarea por defecto"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "prioridad por defecto"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "gravedad por defecto"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "estado de petición por defecto"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "tipo de petición por defecto"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "miembros"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de sprints"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "puntos de historia totales"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "panel de backlog activado"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "panel de kanban activado"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "panel de wiki activo"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "panel de peticiones activo"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de videoconferencia"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "datos extra de videoconferencia"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "creación de plantilla"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "permisos de anónimo"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "permisos de usuario"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "privado"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr "es destacado"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr "está buscando a gente"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr "nota (buscando a gente)"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "colores de etiquetas"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "fecha y hora de actualización"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "recuento"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr "fans la última semana"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr "fans el último mes"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr "fans el último año"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "actividad la última semana"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "actividad el último mes"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configuración de modulos"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "archivado"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "color"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "limite del trabajo en progreso"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "rol por defecto para el propietario"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "opciones por defecto"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "estatuas de historias"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "puntos"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "estatus de tareas"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "estados de petición"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "tipos de petición"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "gravedades"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "roles"
@@ -1932,20 +1974,20 @@ msgstr "Todas"
msgid "None"
msgstr "Ninguna"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "fecha y hora de creación"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "entradas del histórico"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "usuarios notificados"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Observado"
@@ -2687,51 +2729,51 @@ msgid "version"
msgstr "versión"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-"No puedes abandonar este proyecto si no existen mas propietarios del mismo"
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "La dirección de email ya está en uso."
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Rol inválido para el proyecto"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Roles"
@@ -2743,15 +2785,26 @@ msgstr "Sprint futuro"
msgid "Project End"
msgstr "Final de proyecto"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "token inválido"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "No tienes permisos para asignar este sprint a esta tarea."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr "No tienes permisos para asignar esta historia a esta tarea."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "No tienes permisos para asignar este estado a esta tarea."
@@ -2917,6 +2970,223 @@ msgstr ""
"\n"
"[Taiga] Añadido al proyecto '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3163,17 +3433,17 @@ msgstr "Product Owner"
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"No tienes permisos para asignar este sprint a esta historia de usuario."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"No tienes permisos para asignar este estado a esta historia de usuario."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Generada la historia de usuario #{ref} - {subject}"
@@ -3232,11 +3502,11 @@ msgstr "Votos"
msgid "Vote"
msgstr "Voto"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "el parámetro 'content' es obligatório"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "el parámetro 'project_id' es obligatório"
@@ -3256,65 +3526,65 @@ msgstr "Comprueba la API de histórico para obtener el diff exacto"
msgid "Personal info"
msgstr "Información personal"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Permisos"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "datos importántes"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Email duplicado"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Email no válido"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Nombre de usuario o email no válidos"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "¡Correo enviado con éxito!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "token inválido"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "La contraseña actual es obligatoria."
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "La nueva contraseña es obligatoria"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "La longitud de la contraseña debe de ser de al menos 6 caracteres"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Contraseña actual inválida"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invalido, ¿estás seguro de que el token es correcto y no se ha usado antes?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Inválido, ¿estás seguro de que el token es correcto?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "es superusuario"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3322,24 +3592,24 @@ msgstr ""
"Otorga todos los permisos a este usuario sin necesidad de hacerlo "
"explicitamente."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "nombre de usuario"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Introduce un nombre de usuario válido"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "activo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3347,42 +3617,58 @@ msgstr ""
"Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un "
"usuario."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografía"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "fecha de registro"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "idioma por defecto"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "tema por defecto"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "zona horaria por defecto"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "añade color a las etiquetas"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "token de email"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nueva dirección de email"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "permisos"
@@ -3399,6 +3685,22 @@ msgstr "Nombre de usuario inválido. Prueba con otro."
msgid "Username or password does not matches user."
msgstr "Nombre de usuario o contraseña inválidos."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 525eb7d7..bc555044 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -67,7 +67,8 @@ msgid "Error on creating new user."
msgstr "Virhe käyttäjän luonnissa."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Väärä tunniste"
@@ -184,6 +185,15 @@ msgstr ""
"Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli "
"vioittunut."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Sivu ei ole 'viimeinen', ekä sitä pystytä muuntamaan numeroksi."
@@ -241,23 +251,23 @@ msgstr "Virheellinen data"
msgid "No input provided"
msgstr "Syöte puuttuu"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr "En voi luoda uutta kohdetta, vain olemassaolevat voidaan päivittää."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Anna lista kohteista."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Ei löytynyt"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Ei käyttöoikeutta"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Palvelinsovelluksen virhe"
@@ -332,12 +342,12 @@ msgstr "Integrity Error for wrong or invalid arguments"
msgid "Precondition error"
msgstr "Precondition error"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Error in filter params types."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
@@ -479,71 +489,71 @@ msgstr ""
"\n"
"Kommentti: %(comment)s"
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Tarvitaan tiedosto"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Virheellinen tiedostomuoto"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "virhe projektidatan tuonnissa"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "virhe atribuuttilistan tuonnissa"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "virhe oletusarvojen tuonnissa"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "virhe omien arvojen tuonnissa"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "virhe roolien tuonnissa"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "virhe jäsenyyksien tuonnissa"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "virhe kierroksien tuonnissa"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "virhe wiki-sivujen tuonnissa"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "virhe viki-linkkien tuonnissa"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "virhe pyyntöjen tuonnissa"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "virhe käyttäjätarinoiden tuonnissa"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "virhe tehtävien tuonnissa"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "virhe avainsanojen sisäänlukemisessa"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "virhe aikajanojen tuonnissa"
@@ -562,9 +572,7 @@ msgid "It contain invalid custom fields."
msgstr "Sisältää vieheellisiä omia kenttiä."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nimi on tuplana projektille"
@@ -815,11 +823,11 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "nimi"
@@ -832,11 +840,11 @@ msgstr ""
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "kuvaus"
@@ -850,7 +858,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr ""
@@ -858,11 +866,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "koko nimi"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "sähköpostiosoite"
@@ -870,11 +878,11 @@ msgstr "sähköpostiosoite"
msgid "comment"
msgstr "kommentti"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -947,8 +955,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "The payload is not a valid json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projektia ei löydy"
@@ -1264,95 +1272,109 @@ msgstr "Hallinnoi projektin arvoja"
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Puutteelliset argumentit"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Väärä kuvaformaatti"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Virheellinen mallipohjan nimi"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Virheellinen mallipohjan kuvaus"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Vähintään yhden käyttäjän pitää olla aktiivinen ylläpitäjä"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "Projekti ID ei vastaa kohdetta ja projektia"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "omistaja"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "projekti"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "sisältötyyppi"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "objekti ID"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "muokkauspvm"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "liite"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "on poistettu"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "order"
@@ -1373,18 +1395,30 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
+msgid "Multi-Line Text"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr ""
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1544,23 +1578,23 @@ msgstr "suljettu muistiinpano"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Sinulla ei ole oikeuksia laittaa kierrosta tälle pyynnölle."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Sinulla ei ole oikeutta asettaa statusta tälle pyyntö."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Sinulla ei ole oikeutta asettaa vakavuutta tälle pyynnölle."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Sinulla ei ole oikeutta asettaa kiireellisyyttä tälle pyynnölle."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö."
@@ -1614,9 +1648,9 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "hukka-aika"
@@ -1629,8 +1663,8 @@ msgstr "arvioitu alkupvm"
msgid "estimated finish date"
msgstr "arvioitu loppupvm"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "on suljettu"
@@ -1659,224 +1693,232 @@ msgstr "'{param}' parametri on pakollinen"
msgid "'project' parameter is mandatory"
msgstr "'project' parametri on pakollinen"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "sähköposti"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "luo täällä"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "tunniste"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "kutsun lisäteksti"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "käyttäjäjärjestys"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Käyttäjä on jo projektin jäsen"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "oletuspisteet"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "oletus Kt tila"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "oletus tehtävän tila"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "oletus kiireellisyys"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "oletus vakavuus"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "oletus pyynnön tila"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "oletus pyyntö tyyppi"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "jäsenet"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "virstapyväitä yhteensä"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "käyttäjätarinan yhteispisteet"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktiivinen odottavien paneeli"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktiivinen kanban-paneeli"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktiivinen wiki-paneeli"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktiivinen pyyntöpaneeli"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videokokous järjestelmä"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "luo mallipohja"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "vieraan oikeudet"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "käyttäjän oikeudet"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "on yksityinen"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "avainsanojen värit"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "päivityspvm"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "moduulien asetukset"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "on arkistoitu"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "väri"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "työn alla olevien max"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "arvo"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "oletus omistajan rooli"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "oletus optiot"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "kt tilat"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pisteet"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "tehtävän tilat"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "pyyntöjen tilat"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "pyyntötyypit"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "kiireellisyydet"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "vakavuudet"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "roolit"
@@ -1892,20 +1934,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "luontipvm"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "historian kohteet"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "ilmoita käyttäjille"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr ""
@@ -2656,50 +2698,51 @@ msgid "version"
msgstr "versio"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Et voi jättää projektia, jos olet ainoa omistaja"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Sähköpostiosoite on jo käytössä"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Virheellinen rooli projektille"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Roolit"
@@ -2711,15 +2754,26 @@ msgstr "Tuleva kierros"
msgid "Project End"
msgstr "Projektin loppu"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Tunniste on virheellinen"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr ""
@@ -2887,6 +2941,223 @@ msgstr ""
"\n"
"[Taiga] Lisätty projektiin '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3128,15 +3399,15 @@ msgstr "Tuoteomistaja"
msgid "Stakeholder"
msgstr "Sidosryhmä"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -3195,11 +3466,11 @@ msgstr "Ääniä"
msgid "Vote"
msgstr "Äänestä"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content' parametri on pakollinen"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametri on pakollinen"
@@ -3219,134 +3490,150 @@ msgstr ""
msgid "Personal info"
msgstr "Henkilökohtaiset tiedot"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Oikeudet"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Tärkeät päivämäärät"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Sähköposti on jo olemassa"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Virheellinen sähköposti"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Tuntematon käyttäjänimi tai sähköposti"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Sähköposti lähetetty."
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Tunniste on virheellinen"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Nykyinen salasanaparametri tarvitaan"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Uusi salasanaparametri tarvitaan"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Virheellinen nykyinen salasana"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Virheellinen. Oletko varma, että tunniste on oikea ja et ole jo käyttänyt "
"sitä?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Virheellinen, oletko varma että tunniste on oikea?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "pääkäyttäjän status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
"Kertoo että käyttäjä saa tehdä kaiken ilman erikseen annettuja oiekuksia."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "käyttäjänimi"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Vaaditaan. Korkeintaan 30merkkiä. Kirjaimet, numerot ja merkit /./-/_ "
"sallittuja"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Anna olemassa oleva käyttäjänimi."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "aktiivinen"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
"Käyttäjä on aktiivinen. Poista aktiivisuus käyttäjän poistamisen sijaan."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "kuva"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "liittymispvm"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "oletuskieli"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "oletus aikavyöhyke"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "väritä avainsanat"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "sähköpostitunniste"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "uusi sähköpostiosoite"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "oikeudet"
@@ -3363,6 +3650,22 @@ msgstr "Tuntematon käyttäjänimi, yritä uudelleen."
msgid "Username or password does not matches user."
msgstr "Käyttäjätunnus tai salasana eivät ole oikein."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index d5717f2b..d9cb55a4 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -21,9 +21,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-26 14:34+0000\n"
-"Last-Translator: Laurent Cabaret \n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
"MIME-Version: 1.0\n"
@@ -79,7 +79,8 @@ msgid "Error on creating new user."
msgstr "Erreur à la création du nouvel utilisateur."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Jeton invalide"
@@ -207,6 +208,15 @@ msgstr ""
"Envoyez une image valide. Le fichier que vous avez envoyé n'était pas une "
"image ou était une image corrompue."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
@@ -266,25 +276,25 @@ msgstr "Donnée invalide"
msgid "No input provided"
msgstr "Aucune entrée fournie"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Impossible de créer un nouvel élément, seuls les éléments existants peuvent "
"être mis à jour."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Une liste d'éléments était attendue."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Non trouvé"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Permission refusée"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Erreur du serveur d'application"
@@ -359,12 +369,12 @@ msgstr "Erreur d'intégrité ou arguments invalides"
msgid "Precondition error"
msgstr "Erreur de précondition"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Erreur dans les types de paramètres de filtres"
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
@@ -512,72 +522,72 @@ msgstr ""
" Commentaire : %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Veuillez sélectionner au moins un rôle."
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Fichier de dump obligatoire"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Format de dump invalide"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "Erreur lors de l'importation de données"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
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:115
+#: taiga/export_import/dump_service.py:124
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:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "Erreur à l'importation des champs personnalisés"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "Erreur à l'importation des rôles"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "Erreur à l'importation des groupes d'utilisateurs"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "Erreur lors de l'importation des sprints."
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "Erreur à l'importation des pages Wiki"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "Erreur à l'importation des liens Wiki"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "erreur à l'importation des problèmes"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "erreur à l'importation des histoires utilisateur"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "Erreur lors de l'importation des tâches."
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "erreur lors de l'importation des mots-clés"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "erreur lors de l'import des timelines"
@@ -596,9 +606,7 @@ msgid "It contain invalid custom fields."
msgstr "Contient des champs personnalisés non valides."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nom dupliqué pour ce projet"
@@ -831,11 +839,11 @@ msgstr "Authentification requise"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "nom"
@@ -848,11 +856,11 @@ msgstr "Url de l'icône"
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "description"
@@ -863,10 +871,10 @@ msgstr "Url suivante"
#: taiga/external_apps/models.py:42
msgid "secret key for ciphering the application tokens"
-msgstr ""
+msgstr "Clé secrète pour chiffrer le jeton de l'application"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "utilisateur"
@@ -874,11 +882,11 @@ msgstr "utilisateur"
msgid "application"
msgstr "application"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "Nom complet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "Adresse email"
@@ -886,11 +894,11 @@ msgstr "Adresse email"
msgid "comment"
msgstr "Commentaire"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -961,8 +969,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Le payload n'est pas un json valide"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Le projet n'existe pas"
@@ -1107,6 +1115,9 @@ msgid ""
"\n"
"{message}"
msgstr ""
+"Commentaire depuis GitLab:\n"
+"\n"
+"{message}"
#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
#: taiga/permissions/permissions.py:52
@@ -1266,95 +1277,109 @@ msgstr "Administrer les paramètres du projet"
msgid "Admin roles"
msgstr "Administrer les rôles"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "arguments manquants"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "format de l'image non valide"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Nom de modèle non valide"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Description du modèle non valide"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Au moins un utilisateur doit être un administrateur actif"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Mises à jour partielles non supportées"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "propriétaire"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "projet"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "type du contenu"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "identifiant de l'objet"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "état modifié"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "pièces jointes"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "est obsolète"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "ordre"
@@ -1375,18 +1400,30 @@ msgstr "Personnalisé"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Texte"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Texte multi-ligne"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Date"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1546,23 +1583,23 @@ msgstr "note bloquée"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce sprint à ce problème."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Vous n'avez pas la permission d'affecter cette sévérité à ce problème."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Vous n'avez pas la permission d'affecter cette priorité à ce problème."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce type à ce problème."
@@ -1616,9 +1653,9 @@ msgstr "Aimer"
msgid "Likes"
msgstr "Aime"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1631,8 +1668,8 @@ msgstr "date de démarrage estimée"
msgid "estimated finish date"
msgstr "date de fin estimée"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "est fermé"
@@ -1661,224 +1698,232 @@ msgstr "'{param}' paramètre obligatoire"
msgid "'project' parameter is mandatory"
msgstr "'project' paramètre obligatoire"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "Créé le"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "jeton"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "Text supplémentaire de l'invitation"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "classement utilisateur"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "L'utilisateur est déjà un membre du projet"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "Points par défaut"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "statut de l'HU par défaut"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "Etat par défaut des tâches"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "Priorité par défaut"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "Sévérité par défaut"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "statut du problème par défaut"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "type de problème par défaut"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
-msgstr ""
+msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total des jalons"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "total des points d'histoire"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "panneau backlog actif"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "panneau kanban actif"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "panneau wiki actif"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "panneau problèmes actif"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "plateforme de vidéoconférence"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "données complémentaires pour la salle de vidéoconférence"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "Modèle de création"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "Permissions anonymes"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "Permission de l'utilisateur"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "est privé"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr "est mis en avant"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr "est à la recherche de main d'oeuvre"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "couleurs des tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "date de mise à jour"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "total"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr "fans la semaine dernière"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr "fans le mois dernier"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr "fans l'année dernière"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "activité de la semaine écoulée"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "activité du mois écoulé"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "Configurations des modules"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "est archivé"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "couleur"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "limite de travail en cours"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "valeur"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "rôle par défaut du propriétaire"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "options par défaut"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "statuts des us"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "points"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "états des tâches"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "statuts des problèmes"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "types de problèmes"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "priorités"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "sévérités"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "rôles"
@@ -1894,20 +1939,20 @@ msgstr "Toutes"
msgid "None"
msgstr "Aucun"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "date de création"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "entrées dans l'historique"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "notifier les utilisateurs"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Suivre"
@@ -2429,7 +2474,7 @@ msgstr "La version doit être un nombre entier"
#: taiga/projects/occ/mixins.py:59
msgid "The version parameter is not valid"
-msgstr ""
+msgstr "La version n'est pas valide"
#: taiga/projects/occ/mixins.py:75
msgid "The version doesn't match with the current one"
@@ -2440,51 +2485,51 @@ msgid "version"
msgstr "version"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
+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 il n'y a plus d'autres propriétaires"
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Adresse email déjà existante"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Rôle non valide pour le projet"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Rôles"
@@ -2496,17 +2541,28 @@ msgstr "Sprint futurs"
msgid "Project End"
msgstr "Fin du projet"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Jeton invalide"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
-msgstr ""
+msgstr "Vous n'avez pas la permission d'affecter ce sprint à cette tâche."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
-msgstr ""
+msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
-msgstr ""
+msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème."
#: taiga/projects/tasks/models.py:57
msgid "us order"
@@ -2657,6 +2713,223 @@ msgstr ""
"\n"
"[Taiga] Ajouté au projet '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2901,15 +3174,17 @@ msgstr "Product Owner"
msgid "Stakeholder"
msgstr "Participant"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
+"Vous n'avez pas la permission d'affecter ce sprint à ce récit utilisateur."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
+"Vous n'avez pas la permission d'affecter ce statut à ce récit utilisateur."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -2968,11 +3243,11 @@ msgstr "Votes"
msgid "Vote"
msgstr "vote"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content' paramètre obligatoire"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' paramètre obligatoire"
@@ -2992,66 +3267,66 @@ msgstr ""
msgid "Personal info"
msgstr "Informations personnelles"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Permissions"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Dates importantes"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Email dupliquée"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Email non valide"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Nom d'utilisateur ou email non valide"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Mail envoyé avec succès!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Jeton invalide"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Paramètre 'mot de passe actuel' requis"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Paramètre 'nouveau mot de passe' requis"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Le mot de passe doit être d'au moins 6 caractères"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Mot de passe actuel incorrect"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invalide, êtes-vous sûre que le jeton est correct et qu'il n'a pas déjà été "
"utilisé ?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Invalide, êtes-vous sûre que le jeton est correct ?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "statut superutilisateur"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3059,25 +3334,25 @@ msgstr ""
"Indique que l'utilisateur a toutes les permissions sans avoir à lui les "
"donner explicitement"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "nom d'utilisateur"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Obligatoire. 30 caractères maximum. Lettres, nombres et les caractères /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Entrez un nom d'utilisateur valide"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "actif"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3085,42 +3360,58 @@ msgstr ""
"Indique qu'un utilisateur est considéré ou non comme actif. Désélectionnez "
"cette option au lieu de supprimer le compte utilisateur."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biographie"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "photo"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "date d'inscription"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "langage par défaut"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "thème par défaut"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "Fuseau horaire par défaut"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "changer la couleur des tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "jeton email"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nouvelle adresse email"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "permissions"
@@ -3137,6 +3428,22 @@ msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom."
msgid "Username or password does not matches user."
msgstr "Aucun utilisateur avec ce nom ou ce mot de passe."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index c41f6400..773e3463 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,9 +14,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 13:03+0000\n"
-"Last-Translator: F B \n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
"MIME-Version: 1.0\n"
@@ -73,7 +73,8 @@ msgid "Error on creating new user."
msgstr "Errore nella creazione dell'utente."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Token non valido"
@@ -193,6 +194,15 @@ msgstr ""
"Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine "
"o l'immagine potrebbe essere corrotta. "
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La pagina non è 'last', né può essere convertita come int."
@@ -250,25 +260,25 @@ msgstr "Dati non validi"
msgid "No input provided"
msgstr "Non è stato fornito nessun input"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Non è possibile creare un nuovo elemento, solo quelli esistenti possono "
"essere aggiornati"
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Ci si aspetta una lista di oggetti."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Non trovato"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Permesso negato"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Errore sul server"
@@ -344,12 +354,12 @@ msgstr "Errore di integrità causato da un argomento invalido o sbagliato"
msgid "Precondition error"
msgstr "Errore di precondizione"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Errore nel filtro del tipo di parametri."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
@@ -503,72 +513,72 @@ msgstr ""
"\n"
"Commento: %(comment)s"
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "C'è bisogno di almeno un ruolo"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "E' richiesto un file di dump"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Formato di dump invalido"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "Errore nell'importazione del progetto dati"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "Errore nell'importazione della lista degli attributi di progetto"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr ""
"Errore nell'importazione dei valori predefiniti degli attributi del progetto."
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "Errore nell'importazione degli attributi personalizzati"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "Errore nell'importazione i ruoli"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "Errore nell'importazione delle iscrizioni"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "errore nell'importazione degli sprints"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "Errore nell'importazione delle pagine wiki"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "Errore nell'importazione dei link di wiki"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "errore nell'importazione dei problemi"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "Errore nell'importazione delle user story"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "Errore nell'importazione dei compiti "
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "Errore nell'importazione dei tags"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "Errore nell'importazione delle timelines"
@@ -587,9 +597,7 @@ msgid "It contain invalid custom fields."
msgstr "Contiene campi personalizzati invalidi."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Il nome del progetto è duplicato"
@@ -902,11 +910,11 @@ msgstr "E' richiesta l'autenticazione"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "nome"
@@ -919,11 +927,11 @@ msgstr "Url dell'icona"
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrizione"
@@ -937,7 +945,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "chiave segreta per cifrare i token dell'applicazione"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "utente"
@@ -945,11 +953,11 @@ msgstr "utente"
msgid "application"
msgstr "applicazione"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "Nome completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "Inserisci un indirizzo e-mail valido."
@@ -957,11 +965,11 @@ msgstr "Inserisci un indirizzo e-mail valido."
msgid "comment"
msgstr "Commento"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -1038,8 +1046,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Il carico non è un json valido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Il progetto non esiste"
@@ -1398,95 +1406,109 @@ msgstr "Valori dell'amministratore del progetto"
msgid "Admin roles"
msgstr "Ruoli dell'amministratore"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argomento non valido"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato dell'immagine non valido"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Il nome del template non è valido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "La descrizione del template non è valida"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Almeno uno degli utenti deve essere attivo come amministratore"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Aggiornamento non parziale non supportato"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "proprietario"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "progetto"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "tipo di contenuto"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "ID dell'oggetto"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "data modificata"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "file allegato"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "non approvato"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "ordine"
@@ -1507,18 +1529,30 @@ msgstr "Personalizzato"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Testo"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Testo multi-linea"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Data"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1678,23 +1712,23 @@ msgstr "nota bloccata"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Non hai i permessi per aggiungere questo sprint a questo problema"
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Non hai i permessi per aggiungere questo stato a questo problema"
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Non hai i permessi per aggiungere questa criticità a questo problema"
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Non hai i permessi per aggiungere questa priorità a questo problema."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Non hai i permessi per aggiungere questa tipologia a questo problema"
@@ -1748,9 +1782,9 @@ msgstr "Like"
msgid "Likes"
msgstr "Piaciuto"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "lumaca"
@@ -1763,8 +1797,8 @@ msgstr "data stimata di inizio"
msgid "estimated finish date"
msgstr "data stimata di fine"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "è concluso"
@@ -1794,224 +1828,232 @@ msgstr "il parametro '{param}' è obbligatorio"
msgid "'project' parameter is mandatory"
msgstr "il parametro 'project' è obbligatorio"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "creato a "
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "testo ulteriore per l'invito"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "ordine dell'utente"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "L'utente è già membro del progetto"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "punti predefiniti"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "stati predefiniti per le storie utente"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "stati predefiniti del compito"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "priorità predefinita"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "criticità predefinita"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "stato predefinito del problema"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "tipologia predefinita del problema"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membri"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "tappe totali"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "punti totali della storia"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "pannello di backlog attivo"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "pannello kanban attivo"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "pannello wiki attivo"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "pannello dei problemi attivo"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema di videoconferenza"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "ulteriori dati di videoconferenza "
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "creazione del template"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "permessi anonimi"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "permessi dell'utente"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "è privato"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr "in vetrina"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr "sta cercando persone"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr "note sulla ricerca delle persone "
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "colori dei tag"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "tempo e data aggiornati"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "conta"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr "fans nella settimana"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr "fans nel mese"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr "fans nell'anno"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "attività nella settimana"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "attività nel mese"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configurazione dei moduli"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "è archivitato"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "colore"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "limite dei lavori in corso"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "valore"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "ruolo proprietario predefinito"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "opzioni predefinite "
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "stati della storia utente"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punti"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "stati del compito"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "stati del probema"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "tipologie del problema"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "priorità"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "criticità "
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "ruoli"
@@ -2027,20 +2069,20 @@ msgstr "Tutti"
msgid "None"
msgstr "Nessuno"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "tempo e data creati"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "inserimenti della storia"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "notifica utenti"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Osservato"
@@ -2931,50 +2973,51 @@ msgid "version"
msgstr "versione"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Non puoi abbandonare il progetto se non ci sono altri proprietari"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "L'indirizzo email è già usato"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Ruolo di progetto non valido"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Ruoli"
@@ -2986,16 +3029,27 @@ msgstr "Sprint futuri"
msgid "Project End"
msgstr "Termine di progetto"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Token non valido"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "Non hai i permessi per aggiungere questo sprint a questo compito."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Non hai i permessi per aggiungere questa storia utente a questo compito."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "Non hai i permessi per aggiungere questo stato a questo compito."
@@ -3182,6 +3236,223 @@ msgstr ""
"\n"
"[Taiga] aggiunto al progetto '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3425,16 +3696,16 @@ msgstr "Product Owner"
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Non hai i permessi per aggiungere questo sprint a questa storia utente."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr "Non hai i permessi per aggiungere questo stato a questa storia utente."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Stiamo generando la storia utente #{ref} - {subject}"
@@ -3493,11 +3764,11 @@ msgstr "Voti"
msgid "Vote"
msgstr "Voto"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "il parametro 'contenuto' è obbligatorio"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "Il parametro 'ID progetto' è obbligatorio"
@@ -3517,66 +3788,66 @@ msgstr "Controlla le API della storie per la differenza esatta"
msgid "Personal info"
msgstr "Informazioni personali"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Permessi"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Date importanti"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "E-mail duplicata"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "E-mail non valida"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Username o e-mail non validi"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Mail inviata con successo!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Token non valido"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "E' necessario il parametro della password corrente"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "E' necessario il parametro della nuovo password"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Lunghezza della password non valida, sono necessari almeno 6 caratteri"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Password corrente non valida"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Non valido. Sei sicuro che il token sia corretto e che tu non l'abbia già "
"usato in precedenza?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Non valido. Sicuro che il token sia corretto?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "Stato del super-utente"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3584,26 +3855,26 @@ msgstr ""
"Definisce che questo utente ha tutti i permessi senza assegnarglieli "
"esplicitamente."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "nome utente"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Richiede 30 caratteri o meno. Deve comprendere: lettere, numeri e caratteri "
"come /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Inserisci un nome utente valido."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "attivo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3611,42 +3882,58 @@ msgstr ""
"Definisce se questo utente debba essere trattato come attivo. Deseleziona "
"questo invece di eliminare gli account."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "fotografia"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "data di inizio partecipazione"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "lingua predefinita"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "tema predefinito"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "timezone predefinita"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "colora i tag"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "token e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nuovo indirizzo e-mail"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "permessi"
@@ -3663,6 +3950,22 @@ msgstr "Nome utente non valido. Provane uno diverso."
msgid "Username or password does not matches user."
msgstr "Il nome utente o la password non corrispondono all'utente."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index 7e499bcb..aa3f883f 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -66,7 +66,8 @@ msgid "Error on creating new user."
msgstr "Fout bij het aanmaken van een nieuwe gebruiker."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Ongeldig token"
@@ -193,6 +194,15 @@ msgstr ""
"Upload een geldige afbeelding. Het bestand dat je hebt geuploadet was ofwel "
"een afbeelding ofwel een corrupte afbeelding."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Pagina is niet 'last', noch kan het omgezet worden naar een int."
@@ -250,24 +260,24 @@ msgstr "Ongeldige data"
msgid "No input provided"
msgstr "Geen input gegeven"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Kan geen nieuw item aanmaken, enkel bestaande items mogen bijgewerkt worden."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Verwachtte een lijst van items."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Niet gevonden"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Toestemming geweigerd"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Server applicatie fout"
@@ -342,12 +352,12 @@ msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten"
msgid "Precondition error"
msgstr "Preconditie fout"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Fout in filter params types."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
@@ -492,71 +502,71 @@ msgstr ""
" Commentaar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "We hadden minstens één rol nodig"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Dump file nodig"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Ongeldig dump formaat"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "fout bij het importeren van project data"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "fout bij importeren van project attributenlijst"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "fout bij importeren van standaard projectattributen waarden"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "fout bij importeren eigen attributen"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "fout bij importeren rollen"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "fout bij importeren lidmaatschappen"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "fout bij importeren sprints"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "fout bij importeren wiki pagina's"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "fout bij importeren wiki links"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "fout bij importeren issues"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "fout bij importeren user stories"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "fout bij importeren taken"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "fout bij importeren tags"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "fout bij importeren tijdlijnen"
@@ -575,9 +585,7 @@ msgid "It contain invalid custom fields."
msgstr "Het bevat ongeldige eigen velden:"
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Naam gedupliceerd voor het project"
@@ -765,11 +773,11 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "naam"
@@ -782,11 +790,11 @@ msgstr ""
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "omschrijving"
@@ -800,7 +808,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr ""
@@ -808,11 +816,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "volledige naam"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "e-mail adres"
@@ -820,11 +828,11 @@ msgstr "e-mail adres"
msgid "comment"
msgstr "commentaar"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -896,8 +904,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "De payload is geen geldige json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Het project bestaat niet"
@@ -1198,95 +1206,109 @@ msgstr "Admin project waarden"
msgid "Admin roles"
msgstr "Admin rollen"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Onvolledige argumenten"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ongeldig afbeelding formaat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Ongeldige template naam"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Ongeldige template omschrijving"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Minstens één van de gebruikers moet een active admin zijn"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "eigenaar"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "project"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "inhoud type"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "object id"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "gemodifieerde datum"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "bijgevoegd bestand"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "is verouderd"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "volgorde"
@@ -1307,18 +1329,30 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
+msgid "Multi-Line Text"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr ""
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1478,25 +1512,25 @@ msgstr "geblokkeerde notitie"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Je hebt geen toestemming om deze sprint op deze issue te zetten."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Je hebt geen toestemming om deze status toe te kennen aan dze issue."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"Je hebt geen toestemming om dit ernstniveau toe te kennen aan deze issue."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"Je hebt geen toestemming om deze prioriteit toe te kennen aan deze issue."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Je hebt geen toestemming om dit type toe te kennen aan deze issue."
@@ -1550,9 +1584,9 @@ msgstr "Vind ik leuk"
msgid "Likes"
msgstr "Personen die dit leuk vinden"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1565,8 +1599,8 @@ msgstr "geschatte start datum"
msgid "estimated finish date"
msgstr "geschatte datum van afwerking"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "is gesloten"
@@ -1595,224 +1629,232 @@ msgstr "'{param}' parameter is verplicht"
msgid "'project' parameter is mandatory"
msgstr "'project' parameter is verplicht"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "aangemaakt op"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "uitnodiging extra text"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "gebruiker volgorde"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "The gebruikers is al lid van het project"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "standaard punten"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "standaard US status"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "default taak status"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "standaard prioriteit"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "standaard ernstniveau"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "standaard issue status"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "standaard issue type"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "leden"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "totaal van de milestones"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "totaal story points"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "actief backlog paneel"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "actief kanban paneel"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "actief wiki paneel"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "actief issues paneel"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videoconference systeem"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "aanmaak template"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "anonieme toestemmingen"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "gebruikers toestemmingen"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "is privé"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "tag kleuren"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "gewijzigde datum en tijd"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "module config"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "is gearchiveerd"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "kleur"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "work in progress limiet"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "waarde"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "standaard rol eigenaar"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "standaard instellingen"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "us statussen"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punten"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "taak statussen"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "issue statussen"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "issue types"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "prioriteiten"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "ernstniveaus"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "rollen"
@@ -1828,20 +1870,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "aanmaak datum en tijd"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "geschiedenis items"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "verwittig gebruikers"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr ""
@@ -2376,50 +2418,51 @@ msgid "version"
msgstr "versie"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Je kan het project niet verlaten als er geen andere eigenaars zijn"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "E-mail adres is al in gebruik"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Ongeldige rol voor project"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Rollen"
@@ -2431,15 +2474,26 @@ msgstr "Toekomstige sprint"
msgid "Project End"
msgstr "Project einde"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Token is ongeldig"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr ""
@@ -2580,6 +2634,223 @@ msgstr ""
"\n"
"[Taiga] Toegevoegd aan het project '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2823,15 +3094,15 @@ msgstr "Product Owner"
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -2890,11 +3161,11 @@ msgstr "Stemmen"
msgid "Vote"
msgstr "Stem"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'inhoud' parameter is verplicht"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parameter is verplicht"
@@ -2914,64 +3185,64 @@ msgstr ""
msgid "Personal info"
msgstr "Persoonlijke info"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Toestemmingen"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Belangrijke data"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Gedupliceerde e-mail"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Ongeldige e-mail"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Ongeldige gebruikersnaam of e-mail"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Mail met succes verzonden!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Token is ongeldig"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Huidig wachtwoord parameter vereist"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Nieuw wachtwoord parameter vereist"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Ongeldig huidig wachtwoord"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "Ongeldig, weet je zeker dat het token correct en ongebruikt is?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Ongeldig, weet je zeker dat het token correct is?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "superuser status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2979,24 +3250,24 @@ msgstr ""
"Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe "
"te wijzen."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "gebruikersnaam"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Vereist. 30 of minder karakters. Letters, nummers en /./-/_ karakters"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Geef een geldige gebruikersnaam in"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "actief"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3004,42 +3275,58 @@ msgstr ""
"Beduidt of deze gebruiker als actief moet behandeld worden. Deselecteer dit "
"i.p.v. accounts te verwijderen."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografie"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "toetrededatum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "standaard taal"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "standaard tijdzone"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "kleur tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "e-mail token"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nieuw e-mail adres"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "toestemmingen"
@@ -3056,6 +3343,22 @@ msgstr "Ongeldige gebruikersnaam. Probeer met een andere."
msgid "Username or password does not matches user."
msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 0d5a6368..4be40180 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -68,7 +68,8 @@ msgid "Error on creating new user."
msgstr "Błąd przy tworzeniu użytkownika."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Nieprawidłowy token"
@@ -187,6 +188,15 @@ msgstr ""
"Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub "
"jest uszkodzony."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Strona nie jest ostatnią i nie może zostać zmieniona na int."
@@ -244,25 +254,25 @@ msgstr "Nieprawidłowa dana"
msgid "No input provided"
msgstr "Nic nie wpisano"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Nie można utworzyć nowego obiektu, tylko istniejące obiekty mogą być "
"aktualizowane."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Oczekiwana lista elementów."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Nie znaleziono"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Dostęp zabroniony"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Błąd aplikacji serwera"
@@ -337,12 +347,12 @@ msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów"
msgid "Precondition error"
msgstr "Błąd warunków wstępnych"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Błąd w parametrach typów filtrów."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
@@ -493,71 +503,71 @@ msgstr ""
" Komentarz: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Potrzeba conajmiej jednej roli"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Wymagany plik zrzutu"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Nieprawidłowy format zrzutu"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "błąd w trakcie importu danych projektu"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "błąd w trakcie importu atrybutów projektu"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "błąd w trakcie importu domyślnych atrybutów projektu"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "błąd w trakcie importu niestandardowych atrybutów"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "błąd w trakcie importu ról"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "błąd w trakcie importu członkostw"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "błąd w trakcie importu sprintów"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "błąd w trakcie importu stron Wiki"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "błąd w trakcie importu linków Wiki"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "błąd w trakcie importu zgłoszeń"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "błąd w trakcie importu historyjek użytkownika"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "błąd w trakcie importu zadań"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "błąd w trakcie importu tagów"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "błąd w trakcie importu osi czasu"
@@ -576,9 +586,7 @@ msgid "It contain invalid custom fields."
msgstr "Zawiera niewłaściwe pola niestandardowe."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nazwa projektu zduplikowana"
@@ -833,11 +841,11 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "nazwa"
@@ -850,11 +858,11 @@ msgstr ""
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "opis"
@@ -868,7 +876,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "użytkownik"
@@ -876,11 +884,11 @@ msgstr "użytkownik"
msgid "application"
msgstr "aplikacja"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "Imię i Nazwisko"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "adres e-mail"
@@ -888,11 +896,11 @@ msgstr "adres e-mail"
msgid "comment"
msgstr "komentarz"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -964,8 +972,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Źródło nie jest prawidłowym plikiem json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projekt nie istnieje"
@@ -1306,95 +1314,109 @@ msgstr "Administruj wartościami projektu"
msgid "Admin roles"
msgstr "Administruj rolami"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Pola niekompletne"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Niepoprawny format obrazka"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Nieprawidłowa nazwa szablonu"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Nieprawidłowy opis szablonu"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Przynajmniej jeden użytkownik musi być aktywnym Administratorem"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "ID nie pasuje pomiędzy obiektem a projektem"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "właściciel"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "projekt"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "typ zawartości"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "id obiektu"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "data modyfikacji"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "załączony plik"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "jest przestarzałe"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "kolejność"
@@ -1415,18 +1437,30 @@ msgstr "Niestandardowy"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Tekst"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Teks wielowierszowy"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr ""
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1586,23 +1620,23 @@ msgstr "zaglokowana notatka"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Nie masz uprawnień do połączenia tego zgłoszenia ze sprintem."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Nie masz uprawnień do ustawienia statusu dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Nie masz uprawnień do ustawienia ważności dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Nie masz uprawnień do ustawienia priorytetu dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Nie masz uprawnień do ustawienia typu dla tego zgłoszenia."
@@ -1656,9 +1690,9 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1671,8 +1705,8 @@ msgstr "szacowana data rozpoczecia"
msgid "estimated finish date"
msgstr "szacowana data zakończenia"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "jest zamknięte"
@@ -1701,224 +1735,232 @@ msgstr "'{param}' parametr jest obowiązkowy"
msgid "'project' parameter is mandatory"
msgstr "'project' parametr jest obowiązkowy"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "utwórz na"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "dodatkowy tekst w zaproszeniu"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "kolejność użytkowników"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Użytkownik już jest członkiem tego projektu"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "domyślne punkty"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "domyślny status dla HU"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "domyślny status dla zadania"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "domyślny priorytet"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "domyślna ważność"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "domyślny status dla zgłoszenia"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "domyślny typ dla zgłoszenia"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "członkowie"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "wszystkich kamieni milowych"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "wszystkich punktów "
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktywny panel backlog"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktywny panel Kanban"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktywny panel Wiki"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktywny panel zgłoszeń "
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "system wideokonferencji"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "dodatkowe dane dla wideokonferencji"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "szablon "
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "uprawnienia anonimowych"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "uprawnienia użytkownika"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "jest prywatna"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "kolory tagów"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data aktualizacji"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "ilość"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "konfiguracja modułów"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "zarchiwizowane"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "kolor"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "limit postępu prac"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "wartość"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "domyśla rola właściciela"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "domyślne opcje"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "statusy HU"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pinkty"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "statusy zadań"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "statusy zgłoszeń"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "typy zgłoszeń"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "priorytety"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "ważność"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "role"
@@ -1934,20 +1976,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "data utworzenia"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "wpisy historii"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "powiadom użytkowników"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Obserwowane"
@@ -2713,50 +2755,51 @@ msgid "version"
msgstr "wersja"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Nie możesz opuścić projektu, jeśli jesteś jego jedynym właścicielem"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Tena adres e-mail jest już w użyciu"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Nieprawidłowa rola w projekcie"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Role"
@@ -2768,16 +2811,27 @@ msgstr "Przyszły sprint"
msgid "Project End"
msgstr "Zakończenie projektu"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Nieprawidłowy token."
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "Nie masz uprawnień do ustawiania sprintu dla tego zadania."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Nie masz uprawnień do ustawiania historyjki użytkownika dla tego zadania"
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania"
@@ -2946,6 +3000,223 @@ msgstr ""
"\n"
"[Taiga] Dodany do projektu '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3188,17 +3459,17 @@ msgstr "Właściciel produktu"
msgid "Stakeholder"
msgstr "Interesariusz"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Nie masz uprawnień do ustawiania sprintu dla tej historyjki użytkownika."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Nie masz uprawnień do ustawiania statusu do tej historyjki użytkownika."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -3257,11 +3528,11 @@ msgstr "Głosy"
msgid "Vote"
msgstr "Głos"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "Parametr 'zawartość' jest wymagany"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "Parametr 'id_projektu' jest wymagany"
@@ -3281,68 +3552,68 @@ msgstr "Dla pełengo diffa sprawdź API historii"
msgid "Personal info"
msgstr "Informacje osobiste"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Uprawnienia"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Ważne daty"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Zduplikowany adres e-mail"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Niepoprawny adres e-mail"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Nieprawidłowa nazwa użytkownika lub adrs e-mail"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "E-mail wysłany poprawnie!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Nieprawidłowy token."
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Należy podać bieżące hasło"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Należy podać nowe hasło"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr ""
"Nieprawidłowa długość hasła - wymagane jest co najmniej 6 "
"strong>znaków"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Podałeś nieprawidłowe bieżące hasło"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Niepoprawne, jesteś pewien, że token jest poprawny i nie używałeś go "
"wcześniej? "
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Niepoprawne, jesteś pewien, że token jest poprawny?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "status SUPERUSER"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3350,24 +3621,24 @@ msgstr ""
"Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności "
"ich przydzielania."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "nazwa użytkownika"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Wymagane. 30 znaków. Liter, cyfr i znaków /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Wprowadź poprawną nazwę użytkownika"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "aktywny"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3375,42 +3646,58 @@ msgstr ""
"Oznacza, że ten użytkownik ma być traktowany jako aktywny. Możesz to "
"odznaczyć zamiast usuwać konto."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "zdjęcie"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "data dołączenia"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "domyślny język Taiga"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "domyślny szablon Taiga"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "domyśla strefa czasowa"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "kolory tagów"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "tokem e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "nowy adres e-mail"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "uprawnienia"
@@ -3427,6 +3714,22 @@ msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną."
msgid "Username or password does not matches user."
msgstr "Nazwa użytkownika lub hasło są nieprawidłowe"
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 9e3d6598..13da7195 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+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"
@@ -76,7 +76,8 @@ msgid "Error on creating new user."
msgstr "Erro ao criar um novo usuário."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Token inválido"
@@ -194,6 +195,15 @@ msgstr ""
"Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou "
"está corrompido."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Página não é \"última\", nem pode ser convertída para um inteiro."
@@ -251,25 +261,25 @@ msgstr "Dados inválidos"
msgid "No input provided"
msgstr "Nenhuma entrada providenciada"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Não é possível criar um novo item, somente itens já existentes podem ser "
"atualizados."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Esperada uma lista de itens."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Não encontrado"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Permissão negada"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Erro no servidor da aplicação"
@@ -344,12 +354,12 @@ msgstr "Erro de Integridade para argumentos inválidos ou errados"
msgid "Precondition error"
msgstr "Erro de pré-condição"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Erro nos tipos de parâmetros do filtro."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
@@ -501,71 +511,71 @@ msgstr ""
" Comentário: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Nós precisamos de pelo menos uma função"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Necessário de arquivo de restauração"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Formato de aquivo de restauração inválido"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "erro ao importar informações de projeto"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "erro importando lista de atributos do projeto"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "erro importando valores de atributos do projeto padrão"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "erro importando atributos personalizados"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "erro importando funcões"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "erro importando filiações"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "erro importando sprints"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "erro importando páginas wiki"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "erro importando wiki links"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "erro importando casos"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "erro importando user stories"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "erro importando tarefas"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "erro importando tags"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "erro importando linha do tempo"
@@ -584,9 +594,7 @@ msgid "It contain invalid custom fields."
msgstr "Contém campos personalizados inválidos"
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nome duplicado para o projeto"
@@ -840,11 +848,11 @@ msgstr "Autenticação necessária"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nome"
@@ -857,11 +865,11 @@ msgstr "Ícone da url"
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrição"
@@ -875,7 +883,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "chave secreta para cifrar os tokens da aplicação"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "usuário"
@@ -883,11 +891,11 @@ msgstr "usuário"
msgid "application"
msgstr "aplicação"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "nome completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "endereço de e-mail"
@@ -895,11 +903,11 @@ msgstr "endereço de e-mail"
msgid "comment"
msgstr "comentário"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -971,8 +979,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "A carga não é um json válido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "O projeto não existe"
@@ -1312,95 +1320,109 @@ msgstr "Valores projeto admin"
msgid "Admin roles"
msgstr "Funções Admin"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagem inválida"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Nome de template inválido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Descrição de template inválida"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Pelo menos one dos usuários deve ser um administrador ativo"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Atualizações parciais não são suportadas"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "dono"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "projeto"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "tipo de conteúdo"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "identidade de objeto"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "data modificação"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "arquivo anexado"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "está obsoleto"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "ordem"
@@ -1421,18 +1443,30 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Texto"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Multi-linha"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Data"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1592,23 +1626,23 @@ msgstr "nota bloqueada"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Você não tem permissão para colocar esse sprint para esse caso."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Você não tem permissão para colocar esse status para esse caso."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Você não tem permissão para colocar essa severidade para esse caso."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Você não tem permissão para colocar essa prioridade para esse caso."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Você não tem permissão para colocar esse tipo para esse caso."
@@ -1662,9 +1696,9 @@ msgstr "Curtir"
msgid "Likes"
msgstr "Curtidas"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
@@ -1677,8 +1711,8 @@ msgstr "data de início estimada"
msgid "estimated finish date"
msgstr "data de encerramento estimada"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "está fechado"
@@ -1707,224 +1741,232 @@ msgstr "'{param}' parametro é mandatório"
msgid "'project' parameter is mandatory"
msgstr "'project' parametro é mandatório"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "criado em"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "texto extra de convite"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "ordem de usuário"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "O usuário já é membro do projeto"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "pontos padrão"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "status de US padrão"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "status padrão de tarefa"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "prioridade padrão"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "severidade padrão"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "status padrão de caso"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "tipo padrão de caso"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membros"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de marcos de progresso"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "pontos totais de US"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "painel de backlog ativo"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "painel de kanban ativo"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "painel de wiki ativo"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "painel de casos ativo"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de vídeo conferência"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "informação extra de vídeo conferência"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "template de criação"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "permissão anônima"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "permissão de usuário"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "é privado"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "cores de tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data de atualização"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "contagem"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configurações de módulos"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "está arquivado"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "cor"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "trabalho no limite de progresso"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "função padrão para dono "
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "opções padrão"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "status de US"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pontos"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "status de tarefa"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "status de casos"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "tipos de caso"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "severidades"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "funções"
@@ -1940,20 +1982,20 @@ msgstr ""
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "data de criação"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "histórico de entradas"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "notificar usuário"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Observado"
@@ -2697,50 +2739,51 @@ msgid "version"
msgstr "versão"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Você não pode deixar o projeto se não há mais donos"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Endereço de e-mail já utilizado"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Função inválida para projeto"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Funções"
@@ -2752,15 +2795,26 @@ msgstr "Sprint futuro"
msgid "Project End"
msgstr "Fim do projeto"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Token é inválido"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "Você não tem permissão para colocar esse sprint para essa tarefa."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr "Você não tem permissão para colocar essa user story para essa tarefa."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "Você não tem permissão para colocar esse status para essa tarefa."
@@ -2929,6 +2983,223 @@ msgstr ""
"\n"
"[Taiga] Adicionado ao projeto '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3171,15 +3442,15 @@ msgstr "Product Owner"
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr "Você não tem permissão para colocar esse sprint para essa user story."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr "Você não tem permissão para colocar esse status para essa user story."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -3238,11 +3509,11 @@ msgstr "Votos"
msgid "Vote"
msgstr "Vote"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "parâmetro 'conteúdo' é mandatório"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "parametro 'project_id' é mandatório"
@@ -3262,66 +3533,66 @@ msgstr "Verifique o histórico da API para a exata diferença"
msgid "Personal info"
msgstr "Informação pessoal"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Permissões"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Datas importantes"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "E-mail duplicado"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Não é um e-mail válido"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Usuário ou e-mail inválido"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "E-mail enviado com sucesso"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Token é inválido"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Parâmetro de senha atual necessário"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Parâmetro de nova senha necessário"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Comprimento de senha inválido, pelo menos 6 caracteres necessários"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Senha atual inválida"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Inválido, você está certo que o token está correto e não foi usado "
"anteriormente?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Inválido, tem certeza que o token está correto?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "status de superuser"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3329,24 +3600,24 @@ msgstr ""
"Designa que esse usuário tem todas as permissões sem explicitamente assiná-"
"las"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "usuário"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Requerido. 30 caracteres ou menos. Letras, números e caracteres /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Digite um usuário válido"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "ativo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3354,42 +3625,58 @@ msgstr ""
"Designa quando esse usuário deve ser tratado como ativo. desmarque isso em "
"vez de deletar contas."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "data ingressado"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "lingua padrão"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "tema padrão"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "fuso horário padrão"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "tags coloridas"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "token de e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "novo endereço de email"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "permissões"
@@ -3406,6 +3693,22 @@ msgstr "Usuário inválido. Tente com um diferente."
msgid "Username or password does not matches user."
msgstr "Usuário ou senha não correspondem ao usuário"
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index c38ba995..19e89d04 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -9,14 +9,15 @@
# Dmitry Vinokurov , 2015
# Igor Bezukladnikov , 2016
# ilyar, 2016
+# ivan tkachenko , 2016
# Марат , 2015
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-25 18:16+0000\n"
-"Last-Translator: ilyar\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
"MIME-Version: 1.0\n"
@@ -50,7 +51,7 @@ msgstr "Обязательно. 255 символов или меньше. Бук
#: taiga/auth/services.py:74
msgid "Username is already in use."
-msgstr "Это имя пользователя уже используется."
+msgstr "Это имя уже используется."
#: taiga/auth/services.py:77
msgid "Email is already in use."
@@ -73,7 +74,8 @@ msgid "Error on creating new user."
msgstr "Ошибка при создании нового пользователя."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Неверный токен"
@@ -196,6 +198,15 @@ msgstr ""
"Загрузите корректное изображение. Файл, который вы загрузили - либо не "
"изображение, либо не корректное изображение."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Страница не является 'последней' и не может быть приведена к int."
@@ -253,24 +264,24 @@ msgstr "Неправильные данные."
msgid "No input provided"
msgstr "Ввод отсутствует"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Нельзя создать новые объект, только существующие объекты могут быть изменены."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Ожидался список объектов."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Не найдено"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Доступ запрещён"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Ошибка приложения на сервере"
@@ -280,7 +291,7 @@ msgstr "Ошибка соединения."
#: taiga/base/exceptions.py:77
msgid "Malformed request."
-msgstr "Неверное сформированный запрос."
+msgstr "Неверно сформированный запрос."
#: taiga/base/exceptions.py:82
msgid "Incorrect authentication credentials."
@@ -345,12 +356,12 @@ msgstr "Ошибка целостности из-за неправильных
msgid "Precondition error"
msgstr "Ошибка предусловия"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Ошибка в типах фильтров для параметров."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
@@ -502,71 +513,71 @@ msgstr ""
" Комментарий: %(comment)s\n"
" "
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Нам была нужна хотя бы одна роль"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Необходим дамп-файл"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
-msgstr "Неправильный формат для свалки"
+msgstr "Неправильный формат дампа"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
-msgstr "ошибка импорта данных по проекту"
+msgstr "ошибка при импорте данных проекта"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
-msgstr "ошибка импорта списка атрибутов проекта"
+msgstr "ошибка при импорте списков свойств проекта"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
-msgstr "ошибка импорта значение по умолчанию для атрибутов проекта"
+msgstr "ошибка при импорте значений по умолчанию свойств проекта"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
-msgstr "ошибка импорта специальных атрибутов"
+msgstr "ошибка при импорте пользовательских свойств"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
-msgstr "ошибка импорта ролей"
+msgstr "ошибка при импорте ролей"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
-msgstr "ошибка импорта членства"
+msgstr "ошибка при импорте членства"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
-msgstr "ошибка импорта спринтов"
+msgstr "ошибка при импорте спринтов"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
-msgstr "ошибка импорта вики-страниц"
+msgstr "ошибка при импорте вики-страниц"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
-msgstr "ошибка импорта вики-ссылок"
+msgstr "ошибка при импорте вики-ссылок"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
-msgstr "ошибка импорта запросов"
+msgstr "ошибка при импорте запросов"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "ошибка импорта историй от пользователей"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "ошибка импорта задач"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "ошибка импорта тэгов"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "ошибка импорта хронологии проекта"
@@ -585,9 +596,7 @@ msgid "It contain invalid custom fields."
msgstr "Содержит неверные специальные поля"
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Уже есть такое имя для проекта"
@@ -839,11 +848,11 @@ msgstr "Необходима аутентификация"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "имя"
@@ -856,11 +865,11 @@ msgstr "url иконки"
msgid "web"
msgstr "веб"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "описание"
@@ -874,7 +883,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "секретный ключ для шифрования токенов приложения"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "пользователь"
@@ -882,11 +891,11 @@ msgstr "пользователь"
msgid "application"
msgstr "приложение"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "полное имя"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "адрес email"
@@ -894,11 +903,11 @@ msgstr "адрес email"
msgid "comment"
msgstr "комментарий"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -970,8 +979,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Нагрузочный файл не является правильным json-файлом"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Проект не существует"
@@ -1312,96 +1321,109 @@ msgstr "Управлять значениями проекта"
msgid "Admin roles"
msgstr "Управлять ролями"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Неправильный формат изображения"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Неверное название шаблона"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Неверное описание шаблона"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
msgstr ""
-"По крайней мере один пользователь должен быть активным администратором."
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Частичные обновления не поддерживаются"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "Идентификатор проекта не подходит к этому объекту"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "владелец"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "проект"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "тип содержимого"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "идентификатор объекта"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "изменённая дата"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "приложенный файл"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "устаревшее"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "порядок"
@@ -1422,18 +1444,30 @@ msgstr "Специальный"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Текст"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Многострочный текст"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Дата"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1593,27 +1627,27 @@ msgstr "Заметка о блокировке"
msgid "sprint"
msgstr "спринт"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой спринт для этого запроса"
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой статус для этого запроса"
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такую важность для этого запроса"
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой приоритет для этого запроса"
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса"
@@ -1667,9 +1701,9 @@ msgstr "Лайк"
msgid "Likes"
msgstr "Лайки"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "ссылочное имя"
@@ -1682,8 +1716,8 @@ msgstr "предполагаемая дата начала"
msgid "estimated finish date"
msgstr "предполагаемая дата завершения"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "закрыто"
@@ -1714,224 +1748,232 @@ msgstr "параметр '{param}' является обязательным"
msgid "'project' parameter is mandatory"
msgstr "параметр 'project' является обязательным"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "электронная почта"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "создано"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "идентификатор"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "дополнительный текст к приглашению"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "порядок пользователей"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Этот пользователем уже является участником проекта"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "очки по умолчанию"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "статусы ПИ по умолчанию"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "статус задачи по умолчанию"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "приоритет по умолчанию"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "важность по умолчанию"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "статус запроса по умолчанию"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "тип запроса по умолчанию"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "лготип"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "участники"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "общее количество вех"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "очки истории"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "активная панель списка задач"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "активная панель kanban"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "активная wiki-панель"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "панель активных запросов"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "система видеоконференций"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "дополнительные данные системы видеоконференций"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "шаблон для создания"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "права анонимов"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "права пользователя"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "личное"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "цвета тэгов"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "дата и время обновления"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "количество"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "активность за неделю"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "активность за месяц"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "конфигурация модулей"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "архивировано"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "цвет"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "ограничение на активную работу"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "значение"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "роль владельца по умолчанию"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "параметры по умолчанию"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "статусы ПИ"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "очки"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "статусы задач"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "статусы запросов"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "типы запросов"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "приоритеты"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "степени важности"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "роли"
@@ -1947,20 +1989,20 @@ msgstr "Все"
msgid "None"
msgstr "Никаких"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "дата и время создания"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "записи истории"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "уведомить пользователей"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Просмотренные"
@@ -2712,50 +2754,51 @@ msgid "version"
msgstr "версия"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Вы не можете покинуть проект если в нём нет других владельцев"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "Этот почтовый адрес уже используется"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Неверная роль для этого проекта"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Роли"
@@ -2767,16 +2810,27 @@ msgstr "Будущий спринт"
msgid "Project End"
msgstr "Окончание проекта"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Неверный токен"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"У вас нет прав, чтобы назначить эту историю от пользователя этой задаче."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "У вас нет прав, чтобы установить этот статус для этой задачи."
@@ -2947,6 +3001,223 @@ msgstr ""
"\n"
"[Taiga] Добавлены к проекту '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3190,17 +3461,17 @@ msgstr "Владелец продукта"
msgid "Stakeholder"
msgstr "Заинтересованная сторона"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"У вас нет прав чтобы установить спринт для этой пользовательской истории."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"У вас нет прав чтобы установить статус для этой пользовательской истории."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Генерируется пользовательская история #{ref} - {subject}"
@@ -3259,11 +3530,11 @@ msgstr "Голоса"
msgid "Vote"
msgstr "Голосовать"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "параметр 'content' является обязательным"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "параметр 'project_id' является обязательным"
@@ -3283,128 +3554,144 @@ msgstr "Свертесть с историей API для получения и
msgid "Personal info"
msgstr "Личные данные"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Права доступа"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Важные даты"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "Этот email уже используется"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Невалидный email"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Неверное имя пользователя или e-mail"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Письмо успешно отправлено!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Неверный токен"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Поле \"текущий пароль\" является обязательным"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Поле \"новый пароль\" является обязательным"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Неверная длина пароля, требуется как минимум 6 символов"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Неверно указан текущий пароль"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "Неверно, вы уверены что токен правильный и не использовался ранее?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Неверно, вы уверены что токен правильный?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "статус суперпользователя"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "имя пользователя"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Введите корректное имя пользователя."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "активный"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr "Выбранный пользователь активен. Отменить выбор для удаления аккаунта."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "биография"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "фотография"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "когда присоединился"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "язык по умолчанию"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "тема по умолчанию"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "временная зона по умолчанию"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "установить цвета для тэгов"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "email токен"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "новый email адрес"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "разрешения"
@@ -3421,6 +3708,22 @@ msgstr "Неверное имя пользователя. Попробуйте
msgid "Username or password does not matches user."
msgstr "Имя пользователя или пароль не соответствуют пользователю."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index a97be689..26bb0fed 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -65,7 +65,8 @@ msgid "Error on creating new user."
msgstr "Ett fel uppstod når användaren skapades. "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Felaktig förekomst. "
@@ -186,6 +187,15 @@ msgstr ""
"Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild "
"eller en skadad bild."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
@@ -244,24 +254,24 @@ msgstr "Felaktigt data"
msgid "No input provided"
msgstr "Inga indata"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Det går inte att skapa ett nytt objekt, endast befintliga poster uppdateras."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Förväntad lista på poster."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Hittade inte"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "Du har inte behöriget"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Serverprogramfel."
@@ -336,12 +346,12 @@ msgstr "Integritetsfel för felaktiga eller ogiltiga argument"
msgid "Precondition error"
msgstr "Förutsättningsfel"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Fel i filterparametertyper."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
@@ -477,71 +487,71 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "Vi behöver minst en roll"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "Behöver en hämtningsfil"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Invalid hämtningsfilformat"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "fel vid import av projektdata"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "fel vid import av en lista på projektegenskaper"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "fel vid import av standard projektegenskapsvärden"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "fel vid import av anpassade egenskaper"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "fel vid importering av roller"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "fel vid import av medlemskap"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "felaktig import av sprintar"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "vel vid import av wiki-sidor"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "fel vid import av wiki-länkar"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "fel vid import av ärenden"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "fel vid import av användarhistorier"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "fel vid import av uppgifter"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "fel vid importering av taggar"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "fel vid importering av tidslinje"
@@ -560,9 +570,7 @@ msgid "It contain invalid custom fields."
msgstr "Innehåller felaktigt anpassad fält."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Namnet är upprepad för projektet"
@@ -728,11 +736,11 @@ msgstr "Verifiering krävs"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "namn"
@@ -745,11 +753,11 @@ msgstr "Ikonlänk"
msgid "web"
msgstr "Internet"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "beskrivning"
@@ -763,7 +771,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "hemlig nyckel för kryptering av programtecken "
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "användare"
@@ -771,11 +779,11 @@ msgstr "användare"
msgid "application"
msgstr "program"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "hela namnet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "e-postadress"
@@ -783,11 +791,11 @@ msgstr "e-postadress"
msgid "comment"
msgstr "kommentera"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -843,8 +851,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Datasträngen är inte korrekt json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projektet existerar inte"
@@ -1153,95 +1161,109 @@ msgstr "Administrera projektvärden"
msgid "Admin roles"
msgstr "Administratorroller"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Felaktiga argument"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Felaktigt bildformat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Inget giltigt mallnamn"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Inte giltigt mallbeskrivning"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Åtminstone en av användarna måste vara en aktiv administrator"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Delvisa uppdateringar stöds inte. "
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "Projekt-ID stämmer inte mellan objekt och projekt"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "ägare"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "projekt"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "innehållstyp"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "objekt-ID"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "ändrad datum"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "bifogad fil"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "undviks"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "sortera"
@@ -1262,18 +1284,30 @@ msgstr "Anpassa"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Text"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Text med flera rader"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Datum"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1433,23 +1467,23 @@ msgstr "blockerad notering"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Du har inte behörighet att sätta sprinten till det här ärendet."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Du har inte behörighet att sätta status till det här ärendet. "
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Du har inte behörighet att sätta allvarsgrad till det här ärendet. "
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Du har inte behörighet att sätta prioriteten för det här ärendet. "
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Du har inte behörighet att lägga till typen till ärendet. "
@@ -1503,9 +1537,9 @@ msgstr "Gillar"
msgid "Likes"
msgstr "Gillar"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slugg"
@@ -1518,8 +1552,8 @@ msgstr "Beräknad startdatum"
msgid "estimated finish date"
msgstr "Beräknad slutdato"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "är stängd"
@@ -1548,224 +1582,232 @@ msgstr "'{param}' parameter är obligatoriskt"
msgid "'project' parameter is mandatory"
msgstr "'project' parameter är obligatoriskt"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "e-post"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "skapa som"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "textsträng"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "Invitation - extra text"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "användarorder"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Användaren är redan medlem i projekt"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "standardpoäng"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "standard US-poäng"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "standard status för uppgift"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "standard prioritet"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "standard allvarsgrad"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "standard status för ärende"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "standard typ för ärende"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "medlemmar"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "totalt antal milstolpar"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "totalt antal historiepoäng"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktivt panel för inkorg"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktiv kanban-panel"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktiv wiki-panel"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktiv panel för ärenden"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videokonferensssystem"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferens - extra data"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "mall skapas"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "anonyma rättigheter"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "användarbehörigheter"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "är privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "färger för taggar"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "uppdaterad dato och tid"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "räkna"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "konfigurera moduler"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "är arkiverad"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "färg"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "begränsad arbete pågår"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "värde"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "ägarens standardroll"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "standard val"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "US statuser"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "poäng"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "statuser för uppgifter"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "status för ärenden"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "ärendentyper"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "prioriteter"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "allvarsgrad"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "roller"
@@ -1781,20 +1823,20 @@ msgstr "Alla"
msgid "None"
msgstr "Ingen"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "skapad dato och tid"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "historienotat"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "notifiera användare"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "Visad"
@@ -2299,50 +2341,51 @@ msgid "version"
msgstr "version"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Du kan inte lämna projketet om det inte är flera projektägare"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "E-postadressen är redan använd"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Fel roll for projektet"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Roller"
@@ -2354,15 +2397,26 @@ msgstr "Framtidig sprint"
msgid "Project End"
msgstr "Projektslut"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Textsträngen är ogiltig"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "Du har inte behörighet åt att sätta sprinten till en uppgift"
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "Du har inte behörighet att sätta status till en uppgift. "
@@ -2490,6 +2544,223 @@ msgid ""
"[Taiga] Added to the project '%(project)s'\n"
msgstr ""
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2732,18 +3003,18 @@ msgstr "Produktägare"
msgid "Stakeholder"
msgstr "Intressent"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Du har inte behörighet för att lägga sprinten till den här användarhistorien"
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Du har inte behörighet till att sätta den här statusen till "
"användarhistorien."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Skapar användarhistorie #{ref} - {subject}"
@@ -2802,11 +3073,11 @@ msgstr "Röster"
msgid "Vote"
msgstr "Rösta"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content' parametern är obligatoriskt"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametern är obligatoriskt"
@@ -2826,66 +3097,66 @@ msgstr "Kolla historie API för exakt skillnad"
msgid "Personal info"
msgstr "Personalinformation"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "Behörigheter"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Viktiga datum"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "E-post-dublett"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Ingen giltig e-postadress"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Ogiltigt användarnamn eller e-postadress"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "E-posten skickades korrekt"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Textsträngen är ogiltig"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "Parameter för nuvarande lösenord krävs"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Parameter för nytt lösenord krävs"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Felaktig längd på lösenord. Minst 6 alfanumeriska tecken krävs."
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "Fel lösenord"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Fel. Är du säker på att strängen är korrekt och att du inte har använt det "
"tidigare?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Fel, är du säker på att textsträngen är korrekt? "
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "status för administratorn"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2893,25 +3164,25 @@ msgstr ""
"Anger om användaren har alla behörigheter utan att uttryckligen tilldela "
"dem. "
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "användarnamn"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Obligatoriskt. 30 eller färre alfanumeriska tecken, bokstäver och /./-/_ . "
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Skriv in ett giltigt användarnamn"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -2919,42 +3190,58 @@ msgstr ""
"Anger om användaren ska betraktas som aktiv. Avmarkera detta i stället för "
"att ta bort kontot."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biografi"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "blev medlem datum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "standardspråk"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "standardtema"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "standard tidzon"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "farglägg taggar"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "e-poststräng"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "ny e-postadress"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "behörigheter"
@@ -2971,6 +3258,22 @@ msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn."
msgid "Username or password does not matches user."
msgstr "Användarnamn eller lösenord passar inte."
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index b0bfc3c0..ec95da2a 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,9 +10,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-26 14:17+0000\n"
-"Last-Translator: Mert Torun \n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
"MIME-Version: 1.0\n"
@@ -68,7 +68,8 @@ msgid "Error on creating new user."
msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "Geçersiz kupon"
@@ -194,6 +195,15 @@ msgstr ""
"Geçerli bir resim yükleyin. Yüklenen dosya ya bozulmuş bir resim ya da bir "
"resim dosyası değil."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Sayfa 'last'(son) değil, tamsayıya da çevrilemiyor."
@@ -251,23 +261,23 @@ msgstr "Geçersiz veri"
msgid "No input provided"
msgstr "Girdi sağlanmadı"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr "Yeni bir madde oluşturlamıyor, sadece var olanlar güncellenebilir."
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "Bir madde listesi bekleniyor."
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "Bulunamadı"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "İzin verilmedi"
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "Sunucu uygulaması hatası"
@@ -342,12 +352,12 @@ msgstr "Hatalı ya da geçersiz parametreler için Bütünlük Hatası "
msgid "Precondition error"
msgstr "Ön şart hatası"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "Parametre tipleri filtresinde hata."
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
@@ -487,71 +497,71 @@ msgstr ""
"\n"
"Yorumlar: %(comment)s"
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "En azından bir role ihtiyacımız var"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "İhtiyaç duyulan döküm dosyası"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "Geçersiz döküm biçemi"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "İçeri aktarılan proje verisinde hata"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
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:115
+#: taiga/export_import/dump_service.py:124
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:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "özel öznitelikler içeri aktarılırken hata"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "İçeri aktarılan rollerde hata"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "İçeri aktarılan üyeliklerde hata"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "İçeri aktarılan sprintlerde hata"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "İçeri aktarılan wiki sayfalarında hata"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "İçeri aktarılan wiki bağlantılarında hata"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "İçeri aktarılan taleplerde hata"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "İçeri aktarılan görevlerde hata"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "İçeri aktarılan etiketlerde hata"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "zaman çizelgesi içeri aktarılırken hata"
@@ -570,9 +580,7 @@ msgid "It contain invalid custom fields."
msgstr "Geçersiz özel alanlar içeriyor."
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Aynı isimde proje bulunmakta"
@@ -823,11 +831,11 @@ msgstr "Kimlik doğrulama gerekli"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "isim"
@@ -840,11 +848,11 @@ msgstr "İkon url"
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "tanı"
@@ -858,7 +866,7 @@ msgid "secret key for ciphering the application tokens"
msgstr ""
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "kullanıcı"
@@ -866,11 +874,11 @@ msgstr "kullanıcı"
msgid "application"
msgstr "uygulama"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "tam ad"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "e-posta adresi"
@@ -878,11 +886,11 @@ msgstr "e-posta adresi"
msgid "comment"
msgstr "yorum"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -952,8 +960,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr ""
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Proje mevcut değil."
@@ -1260,95 +1268,109 @@ msgstr "Admin proje değerleri"
msgid "Admin roles"
msgstr "Yönetici rolleri"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Eksik parametreq"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Geçersiz resim biçemi"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "Geçersiz şablon adı"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "Geçersiz şablon tanımı"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "Kullanıcılardan en az biri aktif yönetici olmalıdır"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "Kısmi güncellemeler desteklenmiyor"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
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:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "sahip"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "proje"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "içerik tipi"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "nesne id"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "düzenleme tarihi"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "eklenmiş dosya"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "kaldırıldı"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "sıra"
@@ -1369,18 +1391,30 @@ msgstr "Özel"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "Metin"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "Çoklu-satır metin"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "Tarih"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1540,23 +1574,23 @@ msgstr "engellenmiş not"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Bu talep için bu sprinti ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "Bu talep için bu durumu ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "Bu talep için bu kritiklik derecesini ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "Bu talep için bu öncelik durumunu ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "Bu talep için bu tipi ayarlamaya yetkiniz yok."
@@ -1610,9 +1644,9 @@ msgstr "Beğen"
msgid "Likes"
msgstr "Beğeniler"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "satır"
@@ -1625,8 +1659,8 @@ msgstr "yaklaşık başlama tarihi"
msgid "estimated finish date"
msgstr "yaklaşık bitiş tarihi"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "kapatılmış"
@@ -1655,224 +1689,232 @@ msgstr "'{param}' parametresi zorunlu"
msgid "'project' parameter is mandatory"
msgstr "'proje' parametresi zorunlu"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "e-posta"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "kupon"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "Davetiye ekstra metni"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "kullanıcı sırası"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "Kullanıcı zaten projenin üyesi"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "varsayılan puanlar"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "varsayılan KH durumu"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "varsayılan görev durumu"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "varsayılan öncelik"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "varsayılan önem derecesi"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "varsayılan talep durumu"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "varsayılan talep tipi"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "üyeler"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "aşamaların toplamı"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "toplam hikaye puanı"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktif birikmiş iler paneli"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktif kanban paneli"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktif wiki paneli"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktif talep paneli"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "video konferans sistemi"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferans ekstra verisi"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "oluşturma şablonu"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "anonim izinler"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "kullanıcı izinleri"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "gizli"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr "vitrinde"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr "insan arıyor"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "etiket renkleri"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "yükleme tarih-saati"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "sayı"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr "geçen hafta fanları"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr "geçen ayın fanları"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr "geçen yılın fanları"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "geçen haftanın aktiviteleri"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "geçen ayın aktiviteleri"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "modül ayarları"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "arşivlenmiş"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "renk"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "değer"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "varsayılan sahip rolü"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "varsayılan ayarlar"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "kh durumları"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "puanlar"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "görev durumları"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "talep durumları"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "talep tipleri"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "öncelikler"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "önem durumları"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "roller"
@@ -1888,20 +1930,20 @@ msgstr "Hepsi"
msgid "None"
msgstr "Hiçbiri"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "oluşturma tarih-saati"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "tarihçe girdileri"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "kullanıcıları bilgilendir"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "İzlenen"
@@ -2472,50 +2514,51 @@ msgid "version"
msgstr "sürüm"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "Eğer sizden başka sahip/yönetici kalmadıysa projeyi terk edemezsiniz"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "E-posta adresi önceden alınmış"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "Proje için geçersiz rol"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "Roller"
@@ -2527,15 +2570,26 @@ msgstr "Gelecek sprint"
msgid "Project End"
msgstr "Proje Sonu"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "Kupon geçersiz"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "Bu görev için sprint ayarlamanız için izniniz yok."
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok."
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "Bu görev için bu durumu ayarlama izniniz yok."
@@ -2675,6 +2729,223 @@ msgstr ""
"\n"
"[Taiga] '%(project)s' Projesine eklendi\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2907,15 +3178,15 @@ msgstr "Ürün Sahibi"
msgid "Stakeholder"
msgstr "Paydaş"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr "Bu kullanıcı hikayesine bu sprinti ayarlama izniniz yok."
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr "Bu kullanıcı hikayesine bu durumu ayarlama yetkiniz yok."
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
@@ -2974,11 +3245,11 @@ msgstr "Oylar"
msgid "Vote"
msgstr "Oy"
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content' parametresi zorunlu"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametresi zorunlu"
@@ -2998,131 +3269,147 @@ msgstr ""
msgid "Personal info"
msgstr "Kişisel bilgi"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "İzinler"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "Önemli tarihler"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr ""
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "Geçersiz e-posta"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "Geçersiz kullanıcı adı ya da e-posta"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "Posta başarıyla gönderildi!"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "Kupon geçersiz"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr ""
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "Yeni parola parametresi gerekli"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "Geçersiz parola uzunluğu, en az 6 karaktere ihtiyaç var"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr ""
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Geçersiz geçerli bir kupona sahip olduğunuzdan ve bu kuponu daha önce "
"kullanmadığınızdan emin misiniz?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "Geçersiz, kuponun doğru olduğuna emin misin?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "superuser durumu"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "kullanıcı adı"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Zorunlu. 30 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "Geçerli bir kullanıcı adı girin."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "aktif"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "biyografi"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "fotoğraf"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "katılma tarihi"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "varsayılan dil"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "varsayılan tema"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "varsayılan saat dilimi"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "etiketleri renklendir"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "e-posta kuponu"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "yeni e-posta adresi"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "izinler"
@@ -3139,6 +3426,22 @@ msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin."
msgid "Username or password does not matches user."
msgstr "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor"
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index dacffc4b..101df4e7 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,9 +11,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-26 03:45+0000\n"
-"Last-Translator: Chi-Hsun Tsai \n"
+"POT-Creation-Date: 2016-02-19 17:24+0100\n"
+"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
"MIME-Version: 1.0\n"
@@ -68,7 +68,8 @@ msgid "Error on creating new user."
msgstr "無法創建新使用者"
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
+#: taiga/projects/api.py:390
msgid "Invalid token"
msgstr "無效的代碼 "
@@ -180,6 +181,15 @@ msgid ""
"corrupted image."
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
+#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "頁數不是最後,或者它無法轉成整數 "
@@ -237,23 +247,23 @@ msgstr "無效的資料"
msgid "No input provided"
msgstr "無輸入提供"
-#: taiga/base/api/serializers.py:572
+#: taiga/base/api/serializers.py:575
msgid "Cannot create a new item, only existing items may be updated."
msgstr "無法建立新項目,只能更新現有項目"
-#: taiga/base/api/serializers.py:583
+#: taiga/base/api/serializers.py:586
msgid "Expected a list of items."
msgstr "期待的項目清單"
-#: taiga/base/api/views.py:124
+#: taiga/base/api/views.py:125
msgid "Not found"
msgstr "找不到"
-#: taiga/base/api/views.py:127
+#: taiga/base/api/views.py:128
msgid "Permission denied"
msgstr "許可遭拒絕 "
-#: taiga/base/api/views.py:475
+#: taiga/base/api/views.py:476
msgid "Server application error"
msgstr "伺服器應用出錯"
@@ -328,12 +338,12 @@ msgstr "因錯誤或無效參數,一致性出錯"
msgid "Precondition error"
msgstr "前提出錯"
-#: taiga/base/filters.py:78
+#: taiga/base/filters.py:78 taiga/base/filters.py:443
msgid "Error in filter params types."
msgstr "過濾參數類型出錯"
#: taiga/base/filters.py:132 taiga/base/filters.py:231
-#: taiga/projects/filters.py:43
+#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
@@ -484,71 +494,71 @@ msgstr ""
"\n"
"評論: %(comment)s"
-#: taiga/export_import/api.py:105
+#: taiga/export_import/api.py:114
msgid "We needed at least one role"
msgstr "我們至少需要一個角色"
-#: taiga/export_import/api.py:199
+#: taiga/export_import/api.py:216
msgid "Needed dump file"
msgstr "需要的堆存檔案"
-#: taiga/export_import/api.py:206
+#: taiga/export_import/api.py:224
msgid "Invalid dump format"
msgstr "無效堆存格式"
-#: taiga/export_import/dump_service.py:97
+#: taiga/export_import/dump_service.py:106
msgid "error importing project data"
msgstr "滙入重要專案資料出錯"
-#: taiga/export_import/dump_service.py:110
+#: taiga/export_import/dump_service.py:119
msgid "error importing lists of project attributes"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:124
msgid "error importing default project attributes values"
msgstr "滙入預設專案屬性數值出錯"
-#: taiga/export_import/dump_service.py:125
+#: taiga/export_import/dump_service.py:134
msgid "error importing custom attributes"
msgstr "滙入客制性屬出錯"
-#: taiga/export_import/dump_service.py:130
+#: taiga/export_import/dump_service.py:139
msgid "error importing roles"
msgstr "滙入角色出錯"
-#: taiga/export_import/dump_service.py:145
+#: taiga/export_import/dump_service.py:154
msgid "error importing memberships"
msgstr "滙入成員資格出錯"
-#: taiga/export_import/dump_service.py:150
+#: taiga/export_import/dump_service.py:159
msgid "error importing sprints"
msgstr "滙入衝刺任務出錯"
-#: taiga/export_import/dump_service.py:155
+#: taiga/export_import/dump_service.py:164
msgid "error importing wiki pages"
msgstr "滙入維基頁出錯"
-#: taiga/export_import/dump_service.py:160
+#: taiga/export_import/dump_service.py:169
msgid "error importing wiki links"
msgstr "滙入維基連結出錯"
-#: taiga/export_import/dump_service.py:165
+#: taiga/export_import/dump_service.py:174
msgid "error importing issues"
msgstr "滙入問題出錯"
-#: taiga/export_import/dump_service.py:170
+#: taiga/export_import/dump_service.py:179
msgid "error importing user stories"
msgstr "滙入使用者故事出錯"
-#: taiga/export_import/dump_service.py:175
+#: taiga/export_import/dump_service.py:184
msgid "error importing tasks"
msgstr "滙入任務出錯"
-#: taiga/export_import/dump_service.py:180
+#: taiga/export_import/dump_service.py:189
msgid "error importing tags"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing timelines"
msgstr "滙入時間軸出錯"
@@ -567,9 +577,7 @@ msgid "It contain invalid custom fields."
msgstr "包括無效慣例欄位"
#: taiga/export_import/serializers.py:528
-#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70
-#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125
-#: taiga/projects/serializers.py:168
+#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "專案的名稱被複製了"
@@ -820,11 +828,11 @@ msgstr "要求取得授權"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163
-#: taiga/projects/models.py:477 taiga/projects/models.py:516
-#: taiga/projects/models.py:541 taiga/projects/models.py:578
-#: taiga/projects/models.py:601 taiga/projects/models.py:624
-#: taiga/projects/models.py:659 taiga/projects/models.py:682
+#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
#: taiga/users/models.py:251 taiga/webhooks/models.py:28
msgid "name"
msgstr "姓名"
@@ -837,11 +845,11 @@ msgstr "網址圖標"
msgid "web"
msgstr "網頁"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75
+#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167
-#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "描述"
@@ -855,7 +863,7 @@ msgid "secret key for ciphering the application tokens"
msgstr "應用程式的密碼字符數列"
#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51
+#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
msgid "user"
msgstr "使用者"
@@ -863,11 +871,11 @@ msgstr "使用者"
msgid "application"
msgstr "應用程式"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:100
msgid "full name"
msgstr "全名"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:95
msgid "email address"
msgstr "電子郵件"
@@ -875,11 +883,11 @@ msgstr "電子郵件"
msgid "comment"
msgstr "評論"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62
+#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174
-#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87
+#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
#: taiga/userstorage/models.py:28
@@ -948,8 +956,8 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "載荷為無效json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142
-#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "專案不存在"
@@ -1286,95 +1294,109 @@ msgstr "管理員專案數值"
msgid "Admin roles"
msgstr "管理員角色"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:162 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "不完整參數"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:166 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "無效的圖片檔案"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:227
msgid "Not valid template name"
msgstr "非有效樣板名稱 "
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:230
msgid "Not valid template description"
msgstr "無效樣板描述"
-#: taiga/projects/api.py:547 taiga/projects/serializers.py:261
-msgid "At least one of the user must be an active admin"
-msgstr "至少需有一位使用者擔任管理員"
+#: taiga/projects/api.py:345
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:352
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:358
+msgid "The user must be an admin member of the project"
+msgstr ""
+
+#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+
+#: taiga/projects/api.py:697
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
-#: taiga/projects/attachments/api.py:48
+#: taiga/projects/attachments/api.py:51
msgid "Partial updates are not supported"
msgstr "不支援部份更新"
-#: taiga/projects/attachments/api.py:63
+#: taiga/projects/attachments/api.py:66
msgid "Project ID not matches between object and project"
msgstr "專案ID不符合物件與專案"
-#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39
-#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179
-#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38
+#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39
+#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
msgid "owner"
msgstr "所有者"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:40
#: taiga/projects/custom_attributes/models.py:42
#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44
-#: taiga/projects/models.py:465 taiga/projects/models.py:491
-#: taiga/projects/models.py:522 taiga/projects/models.py:551
-#: taiga/projects/models.py:584 taiga/projects/models.py:607
-#: taiga/projects/models.py:634 taiga/projects/models.py:665
-#: taiga/projects/notifications/models.py:72
-#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/notifications/models.py:73
+#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
msgid "project"
msgstr "專案"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:42
msgid "content type"
msgstr "內容類型"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:44
msgid "object id"
msgstr "物件ID"
-#: taiga/projects/attachments/models.py:65
+#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:177 taiga/projects/models.py:691
+#: taiga/projects/models.py:159 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
msgstr "修改日期"
-#: taiga/projects/attachments/models.py:70
+#: taiga/projects/attachments/models.py:55
msgid "attached file"
msgstr "附加檔案"
-#: taiga/projects/attachments/models.py:72
+#: taiga/projects/attachments/models.py:57
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:74
+#: taiga/projects/attachments/models.py:59
msgid "is deprecated"
msgstr "棄用"
-#: taiga/projects/attachments/models.py:76
+#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481
-#: taiga/projects/models.py:518 taiga/projects/models.py:545
-#: taiga/projects/models.py:580 taiga/projects/models.py:603
-#: taiga/projects/models.py:628 taiga/projects/models.py:661
+#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
msgid "order"
msgstr "次序"
@@ -1395,18 +1417,30 @@ msgstr "自定"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:31
+msgid "This project was blocked by staff"
+msgstr ""
+
+#: taiga/projects/choices.py:32
+msgid "This project was blocked because the owner left"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
msgstr "單行文字"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Multi-Line Text"
msgstr "多行列文字"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Date"
msgstr "日期"
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Url"
+msgstr ""
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1566,23 +1600,23 @@ msgstr "封鎖筆記"
msgid "sprint"
msgstr "衝刺任務"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this sprint to this issue."
msgstr "您無權限設定此問題的衝刺任務"
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this status to this issue."
msgstr "您無權限設定此問題的狀態"
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this severity to this issue."
msgstr "您無權限設定此問題的嚴重性"
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this priority to this issue."
msgstr "您無權限設定此問題的優先性"
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:178
msgid "You don't have permissions to set this type to this issue."
msgstr "您無權限設定此問題的類型"
@@ -1636,9 +1670,9 @@ msgstr "喜歡"
msgid "Likes"
msgstr "喜歡"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165
-#: taiga/projects/models.py:479 taiga/projects/models.py:543
-#: taiga/projects/models.py:626 taiga/projects/models.py:684
+#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "代稱"
@@ -1651,8 +1685,8 @@ msgstr "预計開始日期"
msgid "estimated finish date"
msgstr "預計完成日期"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "被關閉"
@@ -1681,224 +1715,232 @@ msgstr "'{param}' 參數為必要"
msgid "'project' parameter is mandatory"
msgstr "'project'參數為必要"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:77
msgid "email"
msgstr "電子郵件"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:79
msgid "create at"
msgstr "創建於"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:81 taiga/users/models.py:117
msgid "token"
msgstr "代號"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:87
msgid "invitation extra text"
msgstr "額外文案邀請"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:90
msgid "user order"
msgstr "使用者次序"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:100
msgid "The user is already member of the project"
msgstr "使用者已是專案成員"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:115
msgid "default points"
msgstr "預設點數"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:119
msgid "default US status"
msgstr "預設使用者故事狀態"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "預設任務狀態"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "預設優先性"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "預設嚴重性"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "預設問題狀態"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "預設議題類型"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "圖標"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:163
msgid "members"
msgstr "成員"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "全部里程碑"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "全部故事點數"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:170 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "活躍的待辦任務優先表面板"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:172 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "活躍的看板式面板"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:174 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "活躍的維基面板"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:176 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "活躍的問題面板"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:179 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "視訊會議系統"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:181 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "視訊會議額外資料"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:186
msgid "creation template"
msgstr "創建模版"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:190
msgid "anonymous permissions"
msgstr "匿名權限"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:194
msgid "user permissions"
msgstr "使用者權限"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:197
msgid "is private"
msgstr "私密"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:200
msgid "is featured"
msgstr " 受矚目的"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:203
msgid "is looking for people"
msgstr "正在找人"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:205
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:217
msgid "tags colors"
msgstr "標籤顏色"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:220
+msgid "project transfer token"
+msgstr ""
+
+#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "更新日期時間"
-#: taiga/projects/models.py:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:227 taiga/projects/models.py:239
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "數量"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:230
msgid "fans last week"
msgstr "上週粉絲"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:233
msgid "fans last month"
msgstr "上個月粉絲"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:236
msgid "fans last year"
msgstr "去年粉絲"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:242
msgid "activity last week"
msgstr "上週活躍成員"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:245
msgid "activity last month"
msgstr "上月活躍成員"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:248
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:466
+#: taiga/projects/models.py:252
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:461
msgid "modules config"
msgstr "模組設定"
-#: taiga/projects/models.py:485
+#: taiga/projects/models.py:480
msgid "is archived"
msgstr "已歸檔"
-#: taiga/projects/models.py:487 taiga/projects/models.py:549
-#: taiga/projects/models.py:582 taiga/projects/models.py:605
-#: taiga/projects/models.py:632 taiga/projects/models.py:663
-#: taiga/users/models.py:119
+#: taiga/projects/models.py:482 taiga/projects/models.py:544
+#: taiga/projects/models.py:577 taiga/projects/models.py:600
+#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: taiga/users/models.py:102
msgid "color"
msgstr "顏色"
-#: taiga/projects/models.py:489
+#: taiga/projects/models.py:484
msgid "work in progress limit"
msgstr "工作進度限制"
-#: taiga/projects/models.py:520 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
msgid "value"
msgstr "價值"
-#: taiga/projects/models.py:694
+#: taiga/projects/models.py:689
msgid "default owner's role"
msgstr "預設所有者角色"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:705
msgid "default options"
msgstr "預設選項"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:706
msgid "us statuses"
msgstr "我們狀況"
-#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "點數"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:708
msgid "task statuses"
msgstr "任務狀況"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:709
msgid "issue statuses"
msgstr "問題狀況"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:710
msgid "issue types"
msgstr "問題類型"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:711
msgid "priorities"
msgstr "優先性"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:712
msgid "severities"
msgstr "嚴重性"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:713
msgid "roles"
msgstr "角色"
@@ -1914,20 +1956,20 @@ msgstr "所有"
msgid "None"
msgstr "無"
-#: taiga/projects/notifications/models.py:62
+#: taiga/projects/notifications/models.py:63
msgid "created date time"
msgstr "創建日期時間"
-#: taiga/projects/notifications/models.py:66
+#: taiga/projects/notifications/models.py:67
msgid "history entries"
msgstr "歷史輸入"
-#: taiga/projects/notifications/models.py:69
+#: taiga/projects/notifications/models.py:70
msgid "notify users"
msgstr "通知用戶"
-#: taiga/projects/notifications/models.py:91
#: taiga/projects/notifications/models.py:92
+#: taiga/projects/notifications/models.py:93
msgid "Watched"
msgstr "已觀注"
@@ -2682,50 +2724,51 @@ msgid "version"
msgstr "版本"
#: taiga/projects/permissions.py:40
-msgid "You can't leave the project if there are no more owners"
-msgstr "如果專案無所有者,你將無法脫離該專案"
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
-#: taiga/projects/serializers.py:237
+#: taiga/projects/serializers.py:168
msgid "Email address is already taken"
msgstr "電子郵件已使用"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:180
msgid "Invalid role for the project"
msgstr "專案無效的角色"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:365
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:366
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:367
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:368
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:369
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:370
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:371
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:372
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:373
msgid "Roles"
msgstr "角色"
@@ -2737,15 +2780,26 @@ msgstr "未來之衝刺"
msgid "Project End"
msgstr "專案結束"
-#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121
+#: taiga/projects/services/transfer.py:61
+#: taiga/projects/services/transfer.py:68
+#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
+#: taiga/users/api.py:174
+msgid "Token is invalid"
+msgstr "代號無效"
+
+#: taiga/projects/services/transfer.py:66
+msgid "Token has expired"
+msgstr ""
+
+#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
msgid "You don't have permissions to set this sprint to this task."
msgstr "無權限更動此任務下的衝刺任務"
-#: taiga/projects/tasks/api.py:115
+#: taiga/projects/tasks/api.py:116
msgid "You don't have permissions to set this user story to this task."
msgstr "無權限更動此務下的使用者故事"
-#: taiga/projects/tasks/api.py:118
+#: taiga/projects/tasks/api.py:119
msgid "You don't have permissions to set this status to this task."
msgstr "無權限更動此任務下的狀態"
@@ -2910,6 +2964,223 @@ msgstr ""
"\n"
"[Taiga] 被加入專案 '%(project)s'\n"
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the new "
+"project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+msgid "This is the reason/comment:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+msgid "This is the reason/comment:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to "
+"a different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+msgid ""
+"\n"
+" This is the reason/comment:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+msgid "Accept or reject the project transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3147,15 +3418,15 @@ msgstr "產品所有人"
msgid "Stakeholder"
msgstr "利害關係人"
-#: taiga/projects/userstories/api.py:162
+#: taiga/projects/userstories/api.py:163
msgid "You don't have permissions to set this sprint to this user story."
msgstr "無權限更動使用者故事的衝刺任務"
-#: taiga/projects/userstories/api.py:166
+#: taiga/projects/userstories/api.py:167
msgid "You don't have permissions to set this status to this user story."
msgstr "無權限更動此使用者故事的狀態"
-#: taiga/projects/userstories/api.py:260
+#: taiga/projects/userstories/api.py:267
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "産生使用者故事 #{ref} - {subject}"
@@ -3214,11 +3485,11 @@ msgstr "投票數"
msgid "Vote"
msgstr "投票 "
-#: taiga/projects/wiki/api.py:67
+#: taiga/projects/wiki/api.py:70
msgid "'content' parameter is mandatory"
msgstr "'content'參數為必要"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:73
msgid "'project_id' parameter is mandatory"
msgstr "'project_id'參數為必要"
@@ -3238,128 +3509,144 @@ msgstr "檢查API過去資料以找出差異"
msgid "Personal info"
msgstr "個人資訊"
-#: taiga/users/admin.py:53
+#: taiga/users/admin.py:54
msgid "Permissions"
msgstr "許可"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:55
+msgid "Restrictions"
+msgstr ""
+
+#: taiga/users/admin.py:57
msgid "Important dates"
msgstr "重要日期"
-#: taiga/users/api.py:112
+#: taiga/users/api.py:113
msgid "Duplicated email"
msgstr "複製電子郵件"
-#: taiga/users/api.py:114
+#: taiga/users/api.py:115
msgid "Not valid email"
msgstr "非有效電子郵性"
-#: taiga/users/api.py:147
+#: taiga/users/api.py:148
msgid "Invalid username or email"
msgstr "無效使用者或郵件"
-#: taiga/users/api.py:156
+#: taiga/users/api.py:157
msgid "Mail sended successful!"
msgstr "成功送出郵件"
-#: taiga/users/api.py:168 taiga/users/api.py:173
-msgid "Token is invalid"
-msgstr "代號無效"
-
-#: taiga/users/api.py:194
+#: taiga/users/api.py:195
msgid "Current password parameter needed"
msgstr "需要目前密碼之參數"
-#: taiga/users/api.py:197
+#: taiga/users/api.py:198
msgid "New password parameter needed"
msgstr "需要新密碼參數"
-#: taiga/users/api.py:200
+#: taiga/users/api.py:201
msgid "Invalid password length at least 6 charaters needed"
msgstr "無效密碼長度,至少需6個字元"
-#: taiga/users/api.py:203
+#: taiga/users/api.py:204
msgid "Invalid current password"
msgstr "無效密碼"
-#: taiga/users/api.py:250 taiga/users/api.py:256
+#: taiga/users/api.py:251 taiga/users/api.py:257
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "無效,請確認代號正確,之前是否曾使用過?"
-#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294
+#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
msgid "Invalid, are you sure the token is correct?"
msgstr "無效,請確認代號是否正確?"
-#: taiga/users/models.py:75
+#: taiga/users/models.py:58
msgid "superuser status"
msgstr "超級使用者狀態 "
-#: taiga/users/models.py:76
+#: taiga/users/models.py:59
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "無經明確分派,即賦予該使用者所有權限,"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:89
msgid "username"
msgstr "使用者名稱"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:90
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "必填。最多30字元(可為數字,字母,符號....)"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:93
msgid "Enter a valid username."
msgstr "輸入有效的使用者名稱 "
-#: taiga/users/models.py:113
+#: taiga/users/models.py:96
msgid "active"
msgstr "活躍"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:97
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr "賦予該使用者活躍角色,以不選擇取代刪除帳戶功能。"
-#: taiga/users/models.py:120
+#: taiga/users/models.py:103
msgid "biography"
msgstr "自傳"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:106
msgid "photo"
msgstr "照片"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:107
msgid "date joined"
msgstr "加入日期"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:109
msgid "default language"
msgstr "預設語言 "
-#: taiga/users/models.py:128
+#: taiga/users/models.py:111
msgid "default theme"
msgstr "預設主題"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:113
msgid "default timezone"
msgstr "預設時區"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:115
msgid "colorize tags"
msgstr "顏色標籤"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:120
msgid "email token"
msgstr "電子郵件符號 "
-#: taiga/users/models.py:139
+#: taiga/users/models.py:122
msgid "new email address"
msgstr "新電子郵件地址"
+#: taiga/users/models.py:129
+msgid "max number of private projects owned"
+msgstr ""
+
+#: taiga/users/models.py:132
+msgid "max number of public projects owned"
+msgstr ""
+
+#: taiga/users/models.py:135
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:139
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
#: taiga/users/models.py:256
msgid "permissions"
msgstr "許可"
@@ -3376,6 +3663,22 @@ msgstr "無效使用者名稱,請重試其它名稱 "
msgid "Username or password does not matches user."
msgstr "用戶名稱與密碼不符"
+#: taiga/users/services.py:590
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:608
+msgid "You have reached the limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:614
+msgid "You have reached the limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
From 5ef1b7d2f34dc682a5b226704185480f306f528e Mon Sep 17 00:00:00 2001
From: Bruno
Date: Sat, 23 Jan 2016 16:13:52 +0800
Subject: [PATCH 047/105] make directory absolute
---
AUTHORS.rst | 1 +
taiga/base/storage.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 3bae5fe3..70045203 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -20,6 +20,7 @@ answer newbie questions, and generally made taiga that much better:
- Andrea Stagi
- Andrés Moya
- Andrey Alekseenko
+- Bruno Clermont
- Chris Wilson
- David Burke
- Hector Colina
diff --git a/taiga/base/storage.py b/taiga/base/storage.py
index 4df5ce25..84c2a460 100644
--- a/taiga/base/storage.py
+++ b/taiga/base/storage.py
@@ -41,7 +41,7 @@ class FileSystemStorage(storage.FileSystemStorage):
# Note that there is a race between os.path.exists and os.makedirs:
# if os.makedirs fails with EEXIST, the directory was created
# concurrently, and we can continue normally. Refs #16082.
- directory = os.path.dirname(name)
+ directory = os.path.join(settings.MEDIA_ROOT, os.path.dirname(name))
if not os.path.exists(directory):
try:
if self.directory_permissions_mode is not None:
From 3a6568e6bcfefa6e6cda08b5c4570588eca57090 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 22 Feb 2016 17:17:17 +0100
Subject: [PATCH 048/105] Add missing import sentence of 'sys.path'
:tophat::sparkles:
---
taiga/projects/management/commands/sample_data.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 83d0d7d3..147ca0a7 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -17,6 +17,8 @@
import random
import datetime
+from os import path
+
from django.core.management.base import BaseCommand
from django.db import transaction
From 190fa3d6766648bbc5e2487c65ac519baa5fa3b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 23 Feb 2016 12:31:52 +0100
Subject: [PATCH 049/105] Fix a problem with mandrill email backend
---
taiga/projects/notifications/services.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 86f5cb27..801215a5 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -466,4 +466,4 @@ def make_ms_thread_index(msg_id, dt):
thread_bin += md5.digest()
# base64 encode
- return base64.b64encode(thread_bin)
+ return str(base64.b64encode(thread_bin))
From a94c1ce58b8bf9e0a8fdd1f1046e97167e84b056 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 23 Feb 2016 15:10:06 +0100
Subject: [PATCH 050/105] Fix a problem with mandrill email backend (the good
solution)
---
taiga/projects/notifications/services.py | 2 +-
tests/integration/test_notifications.py | 8 --------
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 801215a5..9730f0c8 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -466,4 +466,4 @@ def make_ms_thread_index(msg_id, dt):
thread_bin += md5.digest()
# base64 encode
- return str(base64.b64encode(thread_bin))
+ return base64.b64encode(thread_bin).decode("utf-8")
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index 3a8905a3..85ea89fb 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -422,8 +422,6 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai
assert list_id == headers.get('List-ID')
assert 'Thread-Index' in headers
- # always is b64 encoded 22 bytes
- assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
# hashes should match for identical ids and times
# we check the actual method in test_ms_thread_id()
@@ -516,8 +514,6 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail):
assert list_id == headers.get('List-ID')
assert 'Thread-Index' in headers
- # always is b64 encoded 22 bytes
- assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
# hashes should match for identical ids and times
# we check the actual method in test_ms_thread_id()
@@ -610,8 +606,6 @@ def test_send_notifications_using_services_method_for_issues(settings, mail):
assert list_id == headers.get('List-ID')
assert 'Thread-Index' in headers
- # always is b64 encoded 22 bytes
- assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
# hashes should match for identical ids and times
# we check the actual method in test_ms_thread_id()
@@ -703,8 +697,6 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail)
assert list_id == headers.get('List-ID')
assert 'Thread-Index' in headers
- # always is b64 encoded 22 bytes
- assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
# hashes should match for identical ids and times
# we check the actual method in test_ms_thread_id()
From 22d545ebd407ea40b8019aa65a4ff20d00f0fd5d Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 24 Feb 2016 07:57:09 +0100
Subject: [PATCH 051/105] Refactoring code parts where owner really means admin
---
taiga/base/api/permissions.py | 6 +-
taiga/export_import/permissions.py | 6 +-
taiga/permissions/service.py | 2 +-
taiga/projects/api.py | 6 +-
.../projects/custom_attributes/permissions.py | 44 +++----
taiga/projects/history/permissions.py | 6 +-
taiga/projects/issues/permissions.py | 8 +-
taiga/projects/milestones/permissions.py | 6 +-
taiga/projects/permissions.py | 112 +++++++++---------
taiga/projects/references/permissions.py | 2 +-
taiga/projects/serializers.py | 8 +-
taiga/projects/tasks/permissions.py | 8 +-
taiga/projects/userstories/permissions.py | 6 +-
taiga/projects/wiki/permissions.py | 8 +-
taiga/users/permissions.py | 10 +-
taiga/webhooks/permissions.py | 24 ++--
16 files changed, 131 insertions(+), 131 deletions(-)
diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py
index b43d0067..62b40619 100644
--- a/taiga/base/api/permissions.py
+++ b/taiga/base/api/permissions.py
@@ -20,7 +20,7 @@ import abc
from functools import reduce
from taiga.base.utils import sequence as sq
-from taiga.permissions.service import user_has_perm, is_project_owner
+from taiga.permissions.service import user_has_perm, is_project_admin
from django.apps import apps
from django.utils.translation import ugettext as _
@@ -206,9 +206,9 @@ class HasMandatoryParam(PermissionComponent):
return False
-class IsProjectOwner(PermissionComponent):
+class IsProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
- return is_project_owner(request.user, obj)
+ return is_project_admin(request.user, obj)
class IsObjectOwner(PermissionComponent):
diff --git a/taiga/export_import/permissions.py b/taiga/export_import/permissions.py
index 1e0a1dec..22a03ebe 100644
--- a/taiga/export_import/permissions.py
+++ b/taiga/export_import/permissions.py
@@ -17,11 +17,11 @@
from taiga.base.api.permissions import (TaigaResourcePermission,
- IsProjectOwner, IsAuthenticated)
+ IsProjectAdmin, IsAuthenticated)
class ImportExportPermission(TaigaResourcePermission):
import_project_perms = IsAuthenticated()
- import_item_perms = IsProjectOwner()
- export_project_perms = IsProjectOwner()
+ import_item_perms = IsProjectAdmin()
+ export_project_perms = IsProjectAdmin()
load_dump_perms = IsAuthenticated()
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index b452c0b2..a430e082 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -37,7 +37,7 @@ def _get_object_project(obj):
return project
-def is_project_owner(user, obj):
+def is_project_admin(user, obj):
if user.is_superuser:
return True
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 6da6b1f6..b7a712f4 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -144,7 +144,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
else:
project = self.get_object()
- if permissions_service.is_project_owner(self.request.user, project):
+ if permissions_service.is_project_admin(self.request.user, project):
serializer_class = self.admin_serializer_class
return serializer_class
@@ -593,12 +593,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
use_admin_serializer = True
if self.action == "retrieve":
- use_admin_serializer = permissions_service.is_project_owner(self.request.user, self.object.project)
+ use_admin_serializer = permissions_service.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_owner(self.request.user, project)
+ use_admin_serializer = permissions_service.is_project_admin(self.request.user, project)
if use_admin_serializer:
return self.admin_serializer_class
diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py
index 96fe120d..766f9d42 100644
--- a/taiga/projects/custom_attributes/permissions.py
+++ b/taiga/projects/custom_attributes/permissions.py
@@ -17,7 +17,7 @@
from taiga.base.api.permissions import TaigaResourcePermission
from taiga.base.api.permissions import HasProjectPerm
-from taiga.base.api.permissions import IsProjectOwner
+from taiga.base.api.permissions import IsProjectAdmin
from taiga.base.api.permissions import AllowAny
from taiga.base.api.permissions import IsSuperUser
@@ -27,39 +27,39 @@ from taiga.base.api.permissions import IsSuperUser
#######################################################
class UserStoryCustomAttributePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class TaskCustomAttributePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class IssueCustomAttributePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
######################################################
@@ -67,7 +67,7 @@ class IssueCustomAttributePermission(TaigaResourcePermission):
#######################################################
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
update_perms = HasProjectPerm('modify_us')
@@ -75,7 +75,7 @@ class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
update_perms = HasProjectPerm('modify_task')
@@ -83,7 +83,7 @@ class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
class IssueCustomAttributesValuesPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
update_perms = HasProjectPerm('modify_issue')
diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py
index fd139826..015ac22c 100644
--- a/taiga/projects/history/permissions.py
+++ b/taiga/projects/history/permissions.py
@@ -16,10 +16,10 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny,
+ IsProjectAdmin, AllowAny,
IsObjectOwner, PermissionComponent)
-from taiga.permissions.service import is_project_owner
+from taiga.permissions.service import is_project_admin
from taiga.projects.history.services import get_model_from_key, get_pk_from_key
@@ -38,7 +38,7 @@ class IsCommentProjectOwner(PermissionComponent):
model = get_model_from_key(obj.key)
pk = get_pk_from_key(obj.key)
project = model.objects.get(pk=pk)
- return is_project_owner(request.user, project)
+ return is_project_admin(request.user, project)
class UserStoryHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index 07e17201..291dede7 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -17,12 +17,12 @@
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, PermissionComponent,
+ IsProjectAdmin, PermissionComponent,
AllowAny, IsAuthenticated, IsSuperUser)
class IssuePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
create_perms = HasProjectPerm('add_issue')
@@ -49,14 +49,14 @@ class HasIssueIdUrlParam(PermissionComponent):
class IssueVotersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
list_perms = HasProjectPerm('view_issues')
class IssueWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
list_perms = HasProjectPerm('view_issues')
diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py
index 54462605..7404feb0 100644
--- a/taiga/projects/milestones/permissions.py
+++ b/taiga/projects/milestones/permissions.py
@@ -16,12 +16,12 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectOwner, AllowAny,
+ IsAuthenticated, IsProjectAdmin, AllowAny,
IsSuperUser)
class MilestonePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_milestones')
create_perms = HasProjectPerm('add_milestone')
@@ -34,7 +34,7 @@ class MilestonePermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
class MilestoneWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_milestones')
list_perms = HasProjectPerm('view_milestones')
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index 855cc44b..aecf8c3e 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -20,7 +20,7 @@ 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 IsProjectOwner
+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 PermissionComponent
@@ -56,19 +56,19 @@ class ProjectPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
by_slug_perms = HasProjectPerm('view_project')
create_perms = IsAuthenticated()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
- modules_perms = IsProjectOwner()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
+ modules_perms = IsProjectAdmin()
list_perms = AllowAny()
- change_logo_perms = IsProjectOwner()
- remove_logo_perms = IsProjectOwner()
+ change_logo_perms = IsProjectAdmin()
+ remove_logo_perms = IsProjectAdmin()
stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project')
issues_stats_perms = HasProjectPerm('view_project')
- regenerate_userstories_csv_uuid_perms = IsProjectOwner()
- regenerate_issues_csv_uuid_perms = IsProjectOwner()
- regenerate_tasks_csv_uuid_perms = IsProjectOwner()
+ regenerate_userstories_csv_uuid_perms = IsProjectAdmin()
+ regenerate_issues_csv_uuid_perms = IsProjectAdmin()
+ regenerate_tasks_csv_uuid_perms = IsProjectAdmin()
tags_perms = HasProjectPerm('view_project')
tags_colors_perms = HasProjectPerm('view_project')
like_perms = IsAuthenticated() & HasProjectPerm('view_project')
@@ -77,21 +77,21 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
- transfer_request_perms = IsProjectOwner()
+ transfer_request_perms = IsProjectAdmin()
transfer_start_perms = IsMainOwner()
- transfer_reject_perms = IsProjectOwner()
- transfer_accept_perms = IsProjectOwner()
+ transfer_reject_perms = IsProjectAdmin()
+ transfer_accept_perms = IsProjectAdmin()
class ProjectFansPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
list_perms = HasProjectPerm('view_project')
class ProjectWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
list_perms = HasProjectPerm('view_project')
@@ -99,89 +99,89 @@ class ProjectWatchersPermission(TaigaResourcePermission):
class MembershipPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_create_perms = IsProjectOwner()
- resend_invitation_perms = IsProjectOwner()
+ bulk_create_perms = IsProjectAdmin()
+ resend_invitation_perms = IsProjectAdmin()
# User Stories
class PointsPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class UserStoryStatusPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
# Tasks
class TaskStatusPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
# Issues
class SeverityPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class PriorityPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class IssueStatusPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
class IssueTypePermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- bulk_update_order_perms = IsProjectOwner()
+ bulk_update_order_perms = IsProjectAdmin()
# Project Templates
diff --git a/taiga/projects/references/permissions.py b/taiga/projects/references/permissions.py
index a62d5a09..baa91d8c 100644
--- a/taiga/projects/references/permissions.py
+++ b/taiga/projects/references/permissions.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectOwner, AllowAny)
+ IsProjectAdmin, AllowAny)
class ResolverPermission(TaigaResourcePermission):
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 0060ac2e..5650b351 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -33,7 +33,7 @@ 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_owner
+from taiga.permissions.service import is_project_admin
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from . import models
@@ -241,7 +241,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
my_permissions = serializers.SerializerMethodField("get_my_permissions")
- i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
+ i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
i_am_member = serializers.SerializerMethodField("get_i_am_member")
tags = TagsField(default=[], required=False)
@@ -266,9 +266,9 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return get_user_project_permissions(self.context["request"].user, obj)
return []
- def get_i_am_owner(self, obj):
+ def get_i_am_admin(self, obj):
if "request" in self.context:
- return is_project_owner(self.context["request"].user, obj)
+ return is_project_admin(self.context["request"].user, obj)
return False
def get_i_am_member(self, obj):
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 4cb3f691..9e189f10 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -16,12 +16,12 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectOwner, AllowAny,
+ IsAuthenticated, IsProjectAdmin, AllowAny,
IsSuperUser)
class TaskPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
create_perms = HasProjectPerm('add_task')
@@ -39,14 +39,14 @@ class TaskPermission(TaigaResourcePermission):
class TaskVotersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
list_perms = HasProjectPerm('view_tasks')
class TaskWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
list_perms = HasProjectPerm('view_tasks')
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index e5f4af7f..326c99fe 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectOwner,
+ IsAuthenticated, IsProjectAdmin,
AllowAny, IsSuperUser)
@@ -38,14 +38,14 @@ class UserStoryPermission(TaigaResourcePermission):
class UserStoryVotersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
list_perms = HasProjectPerm('view_us')
class UserStoryWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
list_perms = HasProjectPerm('view_us')
diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py
index 2a3c8e3f..d85d56ea 100644
--- a/taiga/projects/wiki/permissions.py
+++ b/taiga/projects/wiki/permissions.py
@@ -16,12 +16,12 @@
# along with this program. If not, see .
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectOwner, AllowAny,
+ IsAuthenticated, IsProjectAdmin, AllowAny,
IsSuperUser)
class WikiPagePermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_wiki_pages')
by_slug_perms = HasProjectPerm('view_wiki_pages')
@@ -36,14 +36,14 @@ class WikiPagePermission(TaigaResourcePermission):
class WikiPageWatchersPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_wiki_pages')
list_perms = HasProjectPerm('view_wiki_pages')
class WikiLinkPermission(TaigaResourcePermission):
- enought_perms = IsProjectOwner() | IsSuperUser()
+ enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_wiki_links')
create_perms = HasProjectPerm('add_wiki_link')
diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py
index a00cce86..c884edf1 100644
--- a/taiga/users/permissions.py
+++ b/taiga/users/permissions.py
@@ -20,7 +20,7 @@ from taiga.base.api.permissions import IsSuperUser
from taiga.base.api.permissions import AllowAny
from taiga.base.api.permissions import IsAuthenticated
from taiga.base.api.permissions import HasProjectPerm
-from taiga.base.api.permissions import IsProjectOwner
+from taiga.base.api.permissions import IsProjectAdmin
from taiga.base.api.permissions import PermissionComponent
@@ -54,8 +54,8 @@ class UserPermission(TaigaResourcePermission):
class RolesPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py
index 7dc41eea..bc1cae61 100644
--- a/taiga/webhooks/permissions.py
+++ b/taiga/webhooks/permissions.py
@@ -15,28 +15,28 @@
# 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, IsProjectOwner,
+from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectAdmin,
AllowAny, PermissionComponent)
-from taiga.permissions.service import is_project_owner
+from taiga.permissions.service import is_project_admin
-class IsWebhookProjectOwner(PermissionComponent):
+class IsWebhookProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
- return is_project_owner(request.user, obj.webhook.project)
+ return is_project_admin(request.user, obj.webhook.project)
class WebhookPermission(TaigaResourcePermission):
- retrieve_perms = IsProjectOwner()
- create_perms = IsProjectOwner()
- update_perms = IsProjectOwner()
- partial_update_perms = IsProjectOwner()
- destroy_perms = IsProjectOwner()
+ retrieve_perms = IsProjectAdmin()
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
- test_perms = IsProjectOwner()
+ test_perms = IsProjectAdmin()
class WebhookLogPermission(TaigaResourcePermission):
- retrieve_perms = IsWebhookProjectOwner()
+ retrieve_perms = IsWebhookProjectAdmin()
list_perms = AllowAny()
- resend_perms = IsWebhookProjectOwner()
+ resend_perms = IsWebhookProjectAdmin()
From 486b03e668002b8a476248bd504d9bbd1c354ec7 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 29 Feb 2016 11:31:23 +0100
Subject: [PATCH 052/105] Fixing travis
---
.travis.yml | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index e18b626a..e07f042c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,15 +7,11 @@ services:
cache:
- apt
- pip
-addons:
- postgresql: "9.4"
- apt:
- sources:
- - deadsnakes
- - ubuntu-toolchain-r-test
- packages:
- - postgresql-plpython-9.4
-before_script:
+before_install:
+ - sudo apt-get -qq update
+ - sudo /etc/init.d/postgresql stop
+ - sudo apt-get install -y postgresql-9.4
+ - sudo apt-get install -y postgresql-plpython-9.4
- psql -c 'create database taiga;' -U postgres
install:
- travis_retry pip install -r requirements-devel.txt
From c92dda9ed066650bc2573e614c430587840ac4cf Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 25 Feb 2016 15:55:31 +0100
Subject: [PATCH 053/105] Adding owner info to project serializer
---
taiga/permissions/service.py | 14 ++++++++++++++
taiga/projects/api.py | 1 +
taiga/projects/serializers.py | 19 ++++++++++++++++---
taiga/users/serializers.py | 2 +-
4 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index a430e082..adf8cb38 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -37,6 +37,20 @@ def _get_object_project(obj):
return project
+def is_project_owner(user, obj):
+ if user.is_superuser:
+ return True
+
+ project = _get_object_project(obj)
+ if project is None:
+ return False
+
+ if user.id == project.owner.id:
+ return True
+
+ return False
+
+
def is_project_admin(user, obj):
if user.is_superuser:
return True
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index b7a712f4..6d057807 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -104,6 +104,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
+ qs = qs.select_related("owner")
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 5650b351..e892f93e 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -33,7 +33,7 @@ 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
+from taiga.permissions.service import is_project_admin, is_project_owner
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from . import models
@@ -130,6 +130,7 @@ class MembershipSerializer(serializers.ModelSerializer):
project_name = serializers.SerializerMethodField("get_project_name")
project_slug = serializers.SerializerMethodField("get_project_slug")
invited_by = UserBasicInfoSerializer(read_only=True)
+ is_owner = serializers.SerializerMethodField("get_is_owner")
class Meta:
model = models.Membership
@@ -147,6 +148,10 @@ class MembershipSerializer(serializers.ModelSerializer):
def get_project_slug(self, obj):
return obj.project.slug if obj and obj.project else ""
+ def get_is_owner(self, obj):
+ return (obj and obj.user_id and obj.project_id and obj.project.owner_id and
+ obj.user_id == obj.project.owner_id)
+
def validate_email(self, attrs, source):
project = attrs.get("project", None)
if project is None:
@@ -241,6 +246,9 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
my_permissions = serializers.SerializerMethodField("get_my_permissions")
+
+ owner = UserBasicInfoSerializer(read_only=True)
+ i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
i_am_member = serializers.SerializerMethodField("get_i_am_member")
@@ -256,7 +264,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
class Meta:
model = models.Project
- read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
+ read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
"transfer_token")
@@ -266,6 +274,11 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return get_user_project_permissions(self.context["request"].user, obj)
return []
+ def get_i_am_owner(self, obj):
+ if "request" in self.context:
+ return is_project_owner(self.context["request"].user, obj)
+ return False
+
def get_i_am_admin(self, obj):
if "request" in self.context:
return is_project_admin(self.context["request"].user, obj)
@@ -341,7 +354,7 @@ class ProjectDetailSerializer(ProjectSerializer):
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
class Meta:
model = models.Project
- read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
+ read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 204852ad..55473a30 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -133,7 +133,7 @@ class UserAdminSerializer(UserSerializer):
class UserBasicInfoSerializer(UserSerializer):
class Meta:
model = User
- fields = ("username", "full_name_display","photo", "big_photo", "is_active")
+ fields = ("username", "full_name_display","photo", "big_photo", "is_active", "id")
class RecoverySerializer(serializers.Serializer):
From 2457e8b70561d229de3dd009d218b2c963616de6 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 1 Mar 2016 09:39:08 +0100
Subject: [PATCH 054/105] Adding project info on about slots when
import/create/edit fails
---
settings/common.py | 4 +++-
taiga/base/exceptions.py | 20 ++++++++++++++++++++
taiga/export_import/api.py | 13 +++----------
taiga/projects/api.py | 15 +++++++++------
tests/integration/test_importer_api.py | 8 ++++++++
tests/integration/test_projects.py | 8 ++++++++
6 files changed, 51 insertions(+), 17 deletions(-)
diff --git a/settings/common.py b/settings/common.py
index e207f057..c871c54c 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -433,7 +433,9 @@ REST_FRAMEWORK = {
# Extra expose header related to Taiga APP (see taiga.base.middleware.cors=)
APP_EXTRA_EXPOSE_HEADERS = [
"taiga-info-total-opened-milestones",
- "taiga-info-total-closed-milestones"
+ "taiga-info-total-closed-milestones",
+ "taiga-info-project-memberships",
+ "taiga-info-project-is-private"
]
DEFAULT_PROJECT_TEMPLATE = "scrum"
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index f81c1424..75b15bd2 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -202,10 +202,27 @@ class NotAuthenticated(NotAuthenticated):
class Blocked(APIException):
+ """
+ Exception used on blocked projects
+ """
status_code = status.HTTP_451_BLOCKED
default_detail = _("Blocked element")
+class NotEnoughSlotsForProject(BaseException):
+ """
+ Exception used on import/edition/creation project errors where the user
+ hasn't slots enough
+ """
+ default_detail = _("Not enough slots for project.")
+
+ def __init__(self, is_private, total_memberships, detail=None):
+ self.detail = detail or self.default_detail
+ self.project_data = {
+ "is_private": is_private,
+ "total_memberships": total_memberships
+ }
+
def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail
@@ -237,6 +254,9 @@ def exception_handler(exc):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait
+ if getattr(exc, "project_data", None):
+ headers["Taiga-Info-Project-Memberships"] = exc.project_data["total_memberships"]
+ headers["Taiga-Info-Project-Is-Private"] = exc.project_data["is_private"]
detail = format_exception(exc)
return response.Response(detail, status=exc.status_code, headers=headers)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index 9eadf5a2..d7466276 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -97,7 +97,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
project=Project(is_private=is_private, id=None)
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error)
# Create Project
project_serialized = service.store_project(data)
@@ -122,7 +122,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
members=max(members, 1)
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
service.store_memberships(project_serialized.object, data)
try:
@@ -224,13 +224,6 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
raise exc.WrongArguments(_("Invalid dump format"))
user = request.user
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- user,
- project=Project(is_private=is_private, id=None)
- )
- if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
-
slug = dump.get('slug', None)
if slug is not None and Project.objects.filter(slug=slug).exists():
del dump['slug']
@@ -242,7 +235,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
members=max(members, 1)
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
if settings.CELERY_ENABLED:
task = tasks.load_project_dump.delay(user, dump)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 6d057807..955fec3e 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -378,7 +378,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
members=members
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
reason = request.DATA.get('reason', None)
services.accept_project_transfer(project, request.user, token, reason)
@@ -414,8 +414,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
def pre_save(self, obj):
user = self.request.user
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(user, project=obj)
+ members = max(obj.memberships.count(), 1)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error)
if not obj.id:
obj.owner = user
@@ -624,13 +625,14 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
# of handling explicit exception catchin here.
if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
+ members = len(data["bulk_memberships"])
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
request.user,
project=project,
- members=len(data["bulk_memberships"])
+ members=members
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
try:
members = services.create_members_in_bulk(data["bulk_memberships"],
@@ -660,13 +662,14 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
def pre_save(self, obj):
if not obj.id:
+ members = 1
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
self.request.user,
project=obj.project,
- members=1
+ members=members
)
if not enough_slots:
- raise exc.BadRequest(not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error)
if not obj.token:
obj.token = str(uuid.uuid1())
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 5d598de8..90fa70dd 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -91,6 +91,8 @@ def test_valid_project_without_enough_public_projects_slots(client):
assert response.status_code == 400
assert "can't have more public projects" in response.data["_error_message"]
assert Project.objects.filter(slug="public-project-without-slots").count() == 0
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "False"
def test_valid_project_without_enough_private_projects_slots(client):
@@ -110,6 +112,8 @@ def test_valid_project_without_enough_private_projects_slots(client):
assert response.status_code == 400
assert "can't have more private projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "True"
assert Project.objects.filter(slug="private-project-without-slots").count() == 0
@@ -1249,6 +1253,8 @@ def test_valid_dump_import_without_enough_public_projects_slots(client):
response = client.post(url, {'dump': data})
assert response.status_code == 400
assert "can't have more public projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "False"
assert Project.objects.filter(slug="public-project-without-slots").count() == 0
@@ -1269,6 +1275,8 @@ def test_valid_dump_import_without_enough_private_projects_slots(client):
response = client.post(url, {'dump': data})
assert response.status_code == 400
assert "can't have more private projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "True"
assert Project.objects.filter(slug="private-project-without-slots").count() == 0
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 3c8f19cc..18219055 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -70,6 +70,8 @@ def test_create_private_project_without_enough_private_projects_slots(client):
assert response.status_code == 400
assert "can't have more private projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "True"
def test_create_public_project_without_enough_public_projects_slots(client):
@@ -86,6 +88,8 @@ def test_create_public_project_without_enough_public_projects_slots(client):
assert response.status_code == 400
assert "can't have more public projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "False"
def test_change_project_from_private_to_public_without_enough_public_projects_slots(client):
@@ -102,6 +106,8 @@ def test_change_project_from_private_to_public_without_enough_public_projects_sl
assert response.status_code == 400
assert "can't have more public projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "False"
def test_change_project_from_public_to_private_without_enough_private_projects_slots(client):
@@ -118,6 +124,8 @@ def test_change_project_from_public_to_private_without_enough_private_projects_s
assert response.status_code == 400
assert "can't have more private projects" in response.data["_error_message"]
+ assert response["Taiga-Info-Project-Memberships"] == "1"
+ assert response["Taiga-Info-Project-Is-Private"] == "True"
def test_create_private_project_with_enough_private_projects_slots(client):
From 432966a378b175d93d36ab4dd33a44046d0ffdae Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 1 Mar 2016 10:22:32 +0100
Subject: [PATCH 055/105] Adding blocked code project info to watched like and
voted
---
taiga/users/serializers.py | 4 ++++
taiga/users/services.py | 6 +++---
tests/integration/test_users.py | 4 ++++
3 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 55473a30..60d9b441 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -198,6 +198,7 @@ class HighLightedContentSerializer(serializers.Serializer):
project_name = serializers.SerializerMethodField("get_project_name")
project_slug = serializers.SerializerMethodField("get_project_slug")
project_is_private = serializers.SerializerMethodField("get_project_is_private")
+ project_blocked_code = serializers.SerializerMethodField("get_project_blocked_code")
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
@@ -245,6 +246,9 @@ class HighLightedContentSerializer(serializers.Serializer):
def get_project_is_private(self, obj):
return self._none_if_project(obj, "project_is_private")
+ def get_project_blocked_code(self, obj):
+ return self._none_if_not_project(obj, "project_blocked_code")
+
def get_logo_small_url(self, obj):
logo = self._none_if_not_project(obj, "logo")
if logo:
diff --git a/taiga/users/services.py b/taiga/users/services.py
index b8bed269..87293c34 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -322,7 +322,7 @@ def get_watched_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
@@ -417,7 +417,7 @@ def get_liked_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{projects_sql}
@@ -500,7 +500,7 @@ def get_voted_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index a2dd0f3f..360c6054 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -498,6 +498,7 @@ def test_get_watched_list_valid_info_for_project():
assert project_watch_info["project_name"] == None
assert project_watch_info["project_slug"] == None
assert project_watch_info["project_is_private"] == None
+ assert project_watch_info["project_blocked_code"] == None
assert project_watch_info["assigned_to_username"] == None
assert project_watch_info["assigned_to_full_name"] == None
assert project_watch_info["assigned_to_photo"] == None
@@ -557,6 +558,7 @@ def test_get_liked_list_valid_info():
assert project_like_info["project_name"] == None
assert project_like_info["project_slug"] == None
assert project_like_info["project_is_private"] == None
+ assert project_like_info["project_blocked_code"] == None
assert project_like_info["assigned_to_username"] == None
assert project_like_info["assigned_to_full_name"] == None
assert project_like_info["assigned_to_photo"] == None
@@ -610,6 +612,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
assert instance_watch_info["project_name"] == instance.project.name
assert instance_watch_info["project_slug"] == instance.project.slug
assert instance_watch_info["project_is_private"] == instance.project.is_private
+ assert instance_watch_info["project_blocked_code"] == instance.project.blocked_code
assert instance_watch_info["assigned_to_username"] == instance.assigned_to.username
assert instance_watch_info["assigned_to_full_name"] == instance.assigned_to.full_name
assert instance_watch_info["assigned_to_photo"] != ""
@@ -666,6 +669,7 @@ def test_get_voted_list_valid_info():
assert instance_vote_info["project_name"] == instance.project.name
assert instance_vote_info["project_slug"] == instance.project.slug
assert instance_vote_info["project_is_private"] == instance.project.is_private
+ assert instance_vote_info["project_blocked_code"] == instance.project.blocked_code
assert instance_vote_info["assigned_to_username"] == instance.assigned_to.username
assert instance_vote_info["assigned_to_full_name"] == instance.assigned_to.full_name
assert instance_vote_info["assigned_to_photo"] != ""
From 66b3e76a70f0a3974d6e3aaf0a3649621e07e6e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 1 Mar 2016 15:13:40 +0100
Subject: [PATCH 056/105] Set total milestone and us points to None if
backlog_module is disabled
---
taiga/projects/models.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 02129b79..5bcb546b 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -286,6 +286,10 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
slug = "{}-{}".format(base_slug, i)
self.slug = slug
+ if not self.is_backlog_activated:
+ self.total_milestones = None
+ self.total_story_points = None
+
if not self.videoconferences:
self.videoconferences_extra_data = None
From 7f7a092b25d9031fbe8bac37fb4689efeaecdf9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Espino?=
Date: Tue, 1 Mar 2016 16:16:17 +0100
Subject: [PATCH 057/105] Add owner to the read-only fields on issues, tasks,
wiki pages and user stories
---
taiga/projects/issues/serializers.py | 2 +-
taiga/projects/tasks/serializers.py | 2 +-
taiga/projects/userstories/serializers.py | 2 +-
taiga/projects/wiki/serializers.py | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 01526a57..c6810a93 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -46,7 +46,7 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
class Meta:
model = models.Issue
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index 5bf022f7..3f159379 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -50,7 +50,7 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
class Meta:
model = models.Task
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
def get_comment(self, obj):
return ""
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index dea31987..0931cae8 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -67,7 +67,7 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, Editab
class Meta:
model = models.UserStory
depth = 0
- read_only_fields = ('created_date', 'modified_date')
+ read_only_fields = ('created_date', 'modified_date', 'owner')
def get_total_points(self, obj):
return obj.get_total_points()
diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py
index 45f91c45..08b58336 100644
--- a/taiga/projects/wiki/serializers.py
+++ b/taiga/projects/wiki/serializers.py
@@ -30,7 +30,7 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri
class Meta:
model = models.WikiPage
- read_only_fields = ('modified_date', 'created_date')
+ read_only_fields = ('modified_date', 'created_date', 'owner')
def get_html(self, obj):
return mdrender(obj.project, obj.content)
From 375ec1c9357fcea7dcd03813b78103d5496fa4c0 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 2 Mar 2016 12:18:56 +0100
Subject: [PATCH 058/105] Fixing watched, like and vote queries
---
taiga/users/services.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 87293c34..64cd5087 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -322,7 +322,7 @@ def get_watched_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
@@ -417,7 +417,7 @@ def get_liked_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{projects_sql}
@@ -500,7 +500,7 @@ def get_voted_list(for_user, from_user, type=None, q=None):
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
- projects_project.blocked_code as projects_project_blocked_code, projects_project.tags_colors, projects_project.logo,
+ projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
From 11a116029ad9c1434fe9596b2a2161ef446d13fa Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 2 Mar 2016 13:18:06 +0100
Subject: [PATCH 059/105] Adding project_blocked_code to any element watched,
liked or voted
---
taiga/users/serializers.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 60d9b441..de95324c 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -198,7 +198,7 @@ class HighLightedContentSerializer(serializers.Serializer):
project_name = serializers.SerializerMethodField("get_project_name")
project_slug = serializers.SerializerMethodField("get_project_slug")
project_is_private = serializers.SerializerMethodField("get_project_is_private")
- project_blocked_code = serializers.SerializerMethodField("get_project_blocked_code")
+ project_blocked_code = serializers.CharField()
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
@@ -246,9 +246,6 @@ class HighLightedContentSerializer(serializers.Serializer):
def get_project_is_private(self, obj):
return self._none_if_project(obj, "project_is_private")
- def get_project_blocked_code(self, obj):
- return self._none_if_not_project(obj, "project_blocked_code")
-
def get_logo_small_url(self, obj):
logo = self._none_if_not_project(obj, "logo")
if logo:
From 6c63d02b17d49ae37c0696b5024d7a6d523c441b Mon Sep 17 00:00:00 2001
From: Yaser Alraddadi
Date: Fri, 19 Feb 2016 03:06:39 +0300
Subject: [PATCH 060/105] usage of cached_property
---
taiga/projects/milestones/api.py | 4 ++--
taiga/projects/milestones/models.py | 18 +++++++-----------
taiga/projects/models.py | 10 +++-------
3 files changed, 12 insertions(+), 20 deletions(-)
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index b4d54023..1728362b 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -116,8 +116,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
'estimated_finish': milestone.estimated_finish,
'total_points': total_points,
'completed_points': milestone.closed_points.values(),
- 'total_userstories': milestone.get_cached_user_stories().count(),
- 'completed_userstories': milestone.get_cached_user_stories().filter(is_closed=True).count(),
+ 'total_userstories': milestone.cached_user_stories.count(),
+ 'completed_userstories': milestone.cached_user_stories.filter(is_closed=True).count(),
'total_tasks': milestone.tasks.count(),
'completed_tasks': milestone.tasks.filter(status__is_closed=True).count(),
'iocaine_doses': milestone.tasks.filter(is_iocaine=True).count(),
diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py
index 65459a6a..1e30d858 100644
--- a/taiga/projects/milestones/models.py
+++ b/taiga/projects/milestones/models.py
@@ -22,6 +22,7 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.exceptions import ValidationError
+from django.utils.functional import cached_property
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.dicts import dict_sum
@@ -57,7 +58,6 @@ class Milestone(WatchedModelMixin, models.Model):
verbose_name=_("order"))
_importing = None
_total_closed_points_by_date = None
- _cached_user_stories = None
class Meta:
verbose_name = "milestone"
@@ -87,13 +87,9 @@ class Milestone(WatchedModelMixin, models.Model):
super().save(*args, **kwargs)
- def get_cached_user_stories(self):
- if self._cached_user_stories is None:
- self._cached_user_stories = self.user_stories.\
- prefetch_related("role_points", "role_points__points").\
- annotate(num_tasks=Count("tasks"))
-
- return self._cached_user_stories
+ @cached_property
+ def cached_user_stories(self):
+ return self.user_stories.prefetch_related("role_points", "role_points__points").annotate(num_tasks=Count("tasks"))
def _get_user_stories_points(self, user_stories):
role_points = [us.role_points.all() for us in user_stories]
@@ -104,13 +100,13 @@ class Milestone(WatchedModelMixin, models.Model):
@property
def total_points(self):
return self._get_user_stories_points(
- [us for us in self.get_cached_user_stories()]
+ [us for us in self.cached_user_stories]
)
@property
def closed_points(self):
return self._get_user_stories_points(
- [us for us in self.get_cached_user_stories() if us.is_closed]
+ [us for us in self.cached_user_stories if us.is_closed]
)
def _get_increment_points(self):
@@ -168,7 +164,7 @@ class Milestone(WatchedModelMixin, models.Model):
# We need to keep the milestone user stories indexed by id in a dict
user_stories = {}
- for us in self.get_cached_user_stories():
+ for us in self.cached_user_stories:
us._total_us_points = sum(self._get_user_stories_points([us]).values())
user_stories[us.id] = us
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 5bcb546b..5cbf5554 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -28,6 +28,7 @@ from django.dispatch import receiver
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
+from django.utils.functional import cached_property
from django_pgjson.fields import JsonField
from djorm_pgarray.fields import TextArrayField
@@ -251,7 +252,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
choices=choices.BLOCKING_CODES + settings.EXTRA_BLOCKING_CODES, default=None,
verbose_name=_("blocked code"))
- _cached_user_stories = None
_importing = None
class Meta:
@@ -341,13 +341,9 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
if save:
self.save()
- @property
+ @cached_property
def cached_user_stories(self):
- print(1111111, self._cached_user_stories)
- if self._cached_user_stories is None:
- self._cached_user_stories = list(self.user_stories.all())
-
- return self._cached_user_stories
+ return list(self.user_stories.all())
def get_roles(self):
return self.roles.all()
From c267ff63a440fd2b3b8009f22ba1ec511137d86f Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 3 Mar 2016 08:30:16 +0100
Subject: [PATCH 061/105] Updating AUTHORS.rst
---
AUTHORS.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 70045203..69fb52ec 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -20,6 +20,7 @@ answer newbie questions, and generally made taiga that much better:
- Andrea Stagi
- Andrés Moya
- Andrey Alekseenko
+- Brett Profitt
- Bruno Clermont
- Chris Wilson
- David Burke
@@ -28,4 +29,4 @@ answer newbie questions, and generally made taiga that much better:
- Julien Palard
- Ricky Posner
- Yamila Moreno
-- Brett Profitt
+- Yaser Alraddadi
From 63eaa8f3c28c3afc5d9ec987cf6fa9a3656135e3 Mon Sep 17 00:00:00 2001
From: luyikei
Date: Sun, 21 Feb 2016 16:22:12 +0900
Subject: [PATCH 062/105] Fix missing import of Http404
---
taiga/projects/api.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 955fec3e..ba72c3e7 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -26,6 +26,7 @@ from django.db.models.functions import Coalesce
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.utils import timezone
+from django.http import Http404
from taiga.base import filters
from taiga.base import response
From 2b3bd1e4bc23498523f2e7b511429b666364bff8 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 3 Mar 2016 08:34:12 +0100
Subject: [PATCH 063/105] Updating AUTHORS.rst
---
AUTHORS.rst | 1 +
1 file changed, 1 insertion(+)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 69fb52ec..b4a52014 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -27,6 +27,7 @@ answer newbie questions, and generally made taiga that much better:
- Hector Colina
- Joe Letts
- Julien Palard
+- luyikei
- Ricky Posner
- Yamila Moreno
- Yaser Alraddadi
From 8d4bd9d65555c45b9d3edd10fcd9a387755170f8 Mon Sep 17 00:00:00 2001
From: luyikei
Date: Sun, 21 Feb 2016 16:45:53 +0900
Subject: [PATCH 064/105] Avoid accessing an attribute of a NoneType object
---
taiga/projects/notifications/mixins.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 4fa143a4..2751b2bd 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -239,13 +239,14 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
def to_native(self, obj):
#if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
- if obj is not None and not hasattr(obj, "watchers"):
- obj.watchers = [user.id for user in obj.get_watchers()]
+ if obj is not None:
+ if not hasattr(obj, "watchers"):
+ obj.watchers = [user.id for user in obj.get_watchers()]
- request = self.context.get("request", None)
- user = request.user if request else None
- if user and user.is_authenticated():
- obj.is_watcher = user.id in obj.watchers
+ request = self.context.get("request", None)
+ user = request.user if request else None
+ if user and user.is_authenticated():
+ obj.is_watcher = user.id in obj.watchers
return super(WatchedResourceModelSerializer, self).to_native(obj)
From 348375a92d893945efeacef7ad4a1b1dd6c806c0 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 2 Mar 2016 10:25:20 +0100
Subject: [PATCH 065/105] Generating blocked projects on sample data
---
.../management/commands/sample_data.py | 27 ++++++++++++++-----
1 file changed, 20 insertions(+), 7 deletions(-)
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 147ca0a7..f5c7b6ea 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -30,11 +30,11 @@ from sampledatahelper.helper import SampleDataHelper
from taiga.users.models import *
from taiga.permissions.permissions import ANON_PERMISSIONS
+from taiga.projects.choices import BLOCKED_BY_STAFF
from taiga.projects.models import *
from taiga.projects.milestones.models import *
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.services.stats import get_stats_for_project
-
from taiga.projects.userstories.models import *
from taiga.projects.tasks.models import *
from taiga.projects.issues.models import *
@@ -105,6 +105,7 @@ NUM_USERS = getattr(settings, "SAMPLE_DATA_NUM_USERS", 10)
NUM_INVITATIONS =getattr(settings, "SAMPLE_DATA_NUM_INVITATIONS", 2)
NUM_PROJECTS =getattr(settings, "SAMPLE_DATA_NUM_PROJECTS", 4)
NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2)
+NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1)
NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5))
NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7))
NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5))
@@ -138,8 +139,19 @@ class Command(BaseCommand):
self.users.append(self.create_user(counter=x))
# create project
- for x in range(NUM_PROJECTS + NUM_EMPTY_PROJECTS):
- project = self.create_project(x, is_private=(x in [2, 4] or self.sd.boolean()))
+ projects_range = range(NUM_PROJECTS + NUM_EMPTY_PROJECTS + NUM_BLOCKED_PROJECTS)
+ empty_projects_range = range(NUM_PROJECTS, NUM_PROJECTS + NUM_EMPTY_PROJECTS )
+ blocked_projects_range = range(
+ NUM_PROJECTS + NUM_EMPTY_PROJECTS,
+ NUM_PROJECTS + NUM_EMPTY_PROJECTS + NUM_BLOCKED_PROJECTS
+ )
+
+ for x in projects_range:
+ project = self.create_project(
+ x,
+ is_private=(x in [2, 4] or self.sd.boolean()),
+ blocked_code = BLOCKED_BY_STAFF if x in(blocked_projects_range) else None
+ )
# added memberships
computable_project_roles = set()
@@ -194,8 +206,8 @@ class Command(BaseCommand):
project=project,
order=i)
-
- if x < NUM_PROJECTS:
+ # If the project isn't empty
+ if x not in empty_projects_range:
start_date = now() - datetime.timedelta(55)
# create milestones
@@ -457,7 +469,7 @@ class Command(BaseCommand):
return milestone
- def create_project(self, counter, is_private=None):
+ def create_project(self, counter, is_private=None, blocked_code=None):
if is_private is None:
is_private=self.sd.boolean()
@@ -475,7 +487,8 @@ class Command(BaseCommand):
tags=self.sd.words(1, 10).split(" "),
is_looking_for_people=counter in LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS,
looking_for_people_note=self.sd.short_sentence(),
- is_featured=counter in FEATURED_PROJECTS_POSITIONS)
+ is_featured=counter in FEATURED_PROJECTS_POSITIONS,
+ blocked_code=blocked_code)
project.is_kanban_activated = True
project.save()
From d8c97201030ff274fd016d12c89fc53acfe72c73 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Thu, 3 Mar 2016 12:32:57 +0100
Subject: [PATCH 066/105] Fixing mdrender import
---
taiga/export_import/serializers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py
index ceb8263f..a2a8663e 100644
--- a/taiga/export_import/serializers.py
+++ b/taiga/export_import/serializers.py
@@ -29,10 +29,10 @@ from django.utils.translation import ugettext as _
from django.contrib.contenttypes.models import ContentType
-from taiga import mdrender
from taiga.base.api import serializers
from taiga.base.fields import JsonField, PgArrayField
+from taiga.mdrender.service import render as mdrender
from taiga.projects import models as projects_models
from taiga.projects.custom_attributes import models as custom_attributes_models
from taiga.projects.userstories import models as userstories_models
@@ -155,7 +155,7 @@ class CommentField(serializers.WritableField):
def field_from_native(self, data, files, field_name, into):
super().field_from_native(data, files, field_name, into)
- into["comment_html"] = mdrender.render(self.context['project'], data.get("comment", ""))
+ into["comment_html"] = mdrender(self.context['project'], data.get("comment", ""))
class ProjectRelatedField(serializers.RelatedField):
From 702c306ad75e0502f9daaf257136cd309bb4d0f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 3 Mar 2016 11:26:28 +0100
Subject: [PATCH 067/105] Add django-redis
---
.travis.yml | 1 +
requirements.txt | 1 +
settings/common.py | 2 ++
settings/local.py.example | 11 +++++++++++
settings/testing.py | 10 ++++++++++
settings/travis.py | 10 ++++++++++
6 files changed, 35 insertions(+)
diff --git a/.travis.yml b/.travis.yml
index e07f042c..f539f181 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ python:
- "3.5"
services:
- rabbitmq # will start rabbitmq-server
+ - redis-server
cache:
- apt
- pip
diff --git a/requirements.txt b/requirements.txt
index 181e863f..39a7c8ee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,6 +11,7 @@ amqp==1.4.9
djmail==0.12.0.post1
django-pgjson==0.3.1
djorm-pgarray==1.2
+django-redis==4.3.0
django-jinja==2.1.2
jinja2==2.8
pygments==2.0.2
diff --git a/settings/common.py b/settings/common.py
index c871c54c..fd3631a4 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -35,6 +35,7 @@ DATABASES = {
}
}
+# Default cache (you can use redis cache, see settings/locale.py.example)
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
@@ -42,6 +43,7 @@ CACHES = {
}
}
+
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]
diff --git a/settings/local.py.example b/settings/local.py.example
index d0052032..e22a7ec0 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -33,6 +33,17 @@ DATABASES = {
}
}
+# You can use redis cache
+#CACHES = {
+# "default": {
+# "BACKEND": "django_redis.cache.RedisCache",
+# "LOCATION": "'redis://localhost:6379/1",
+# "OPTIONS": {
+# "CLIENT_CLASS": "django_redis.client.DefaultClient",
+# }
+# }
+#}
+
#SITES = {
# "api": {
# "scheme": "http",
diff --git a/settings/testing.py b/settings/testing.py
index b1549a8a..abb66205 100644
--- a/settings/testing.py
+++ b/settings/testing.py
@@ -17,6 +17,16 @@
from .development import *
+CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "'redis://localhost:6379/1",
+ "OPTIONS": {
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
+ }
+ }
+}
+
SKIP_SOUTH_TESTS = True
SOUTH_TESTS_MIGRATE = False
CELERY_ALWAYS_EAGER = True
diff --git a/settings/travis.py b/settings/travis.py
index 8481688c..f0a56652 100644
--- a/settings/travis.py
+++ b/settings/travis.py
@@ -24,3 +24,13 @@ DATABASES = {
'USERNAME': 'postgres',
}
}
+
+CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "'redis://localhost:6379/1",
+ "OPTIONS": {
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
+ }
+ }
+}
From 000490bbc0c5a18412328b386fd1982dcefcf727 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 8 Mar 2016 15:14:20 +0100
Subject: [PATCH 068/105] Revert "Add django-redis"
This reverts commit 702c306ad75e0502f9daaf257136cd309bb4d0f3.
---
.travis.yml | 1 -
requirements.txt | 1 -
settings/common.py | 2 --
settings/local.py.example | 11 -----------
settings/testing.py | 10 ----------
settings/travis.py | 10 ----------
6 files changed, 35 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index f539f181..e07f042c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,7 +4,6 @@ python:
- "3.5"
services:
- rabbitmq # will start rabbitmq-server
- - redis-server
cache:
- apt
- pip
diff --git a/requirements.txt b/requirements.txt
index 39a7c8ee..181e863f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,6 @@ amqp==1.4.9
djmail==0.12.0.post1
django-pgjson==0.3.1
djorm-pgarray==1.2
-django-redis==4.3.0
django-jinja==2.1.2
jinja2==2.8
pygments==2.0.2
diff --git a/settings/common.py b/settings/common.py
index fd3631a4..c871c54c 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -35,7 +35,6 @@ DATABASES = {
}
}
-# Default cache (you can use redis cache, see settings/locale.py.example)
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
@@ -43,7 +42,6 @@ CACHES = {
}
}
-
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]
diff --git a/settings/local.py.example b/settings/local.py.example
index e22a7ec0..d0052032 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -33,17 +33,6 @@ DATABASES = {
}
}
-# You can use redis cache
-#CACHES = {
-# "default": {
-# "BACKEND": "django_redis.cache.RedisCache",
-# "LOCATION": "'redis://localhost:6379/1",
-# "OPTIONS": {
-# "CLIENT_CLASS": "django_redis.client.DefaultClient",
-# }
-# }
-#}
-
#SITES = {
# "api": {
# "scheme": "http",
diff --git a/settings/testing.py b/settings/testing.py
index abb66205..b1549a8a 100644
--- a/settings/testing.py
+++ b/settings/testing.py
@@ -17,16 +17,6 @@
from .development import *
-CACHES = {
- "default": {
- "BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": "'redis://localhost:6379/1",
- "OPTIONS": {
- "CLIENT_CLASS": "django_redis.client.DefaultClient",
- }
- }
-}
-
SKIP_SOUTH_TESTS = True
SOUTH_TESTS_MIGRATE = False
CELERY_ALWAYS_EAGER = True
diff --git a/settings/travis.py b/settings/travis.py
index f0a56652..8481688c 100644
--- a/settings/travis.py
+++ b/settings/travis.py
@@ -24,13 +24,3 @@ DATABASES = {
'USERNAME': 'postgres',
}
}
-
-CACHES = {
- "default": {
- "BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": "'redis://localhost:6379/1",
- "OPTIONS": {
- "CLIENT_CLASS": "django_redis.client.DefaultClient",
- }
- }
-}
From 8c82eda1bec5ba8a7057c6262755d27519cd7c35 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 8 Mar 2016 14:46:51 +0100
Subject: [PATCH 069/105] Fixing memory leak on export related to timeline
---
taiga/export_import/serializers.py | 6 ------
taiga/export_import/service.py | 19 +++++++++++++++++--
2 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py
index a2a8663e..49d574ca 100644
--- a/taiga/export_import/serializers.py
+++ b/taiga/export_import/serializers.py
@@ -43,7 +43,6 @@ from taiga.projects.wiki import models as wiki_models
from taiga.projects.history import models as history_models
from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models
-from taiga.timeline import service as timeline_service
from taiga.users import models as users_models
from taiga.projects.notifications import services as notifications_services
from taiga.projects.votes import services as votes_service
@@ -674,12 +673,7 @@ class ProjectExportSerializer(WatcheableObjectModelSerializer):
issues = IssueExportSerializer(many=True, required=False)
wiki_links = WikiLinkExportSerializer(many=True, required=False)
wiki_pages = WikiPageExportSerializer(many=True, required=False)
- timeline = serializers.SerializerMethodField("get_timeline")
class Meta:
model = projects_models.Project
exclude = ('id', 'creation_template', 'members')
-
- def get_timeline(self, obj):
- timeline_qs = timeline_service.get_project_timeline(obj)
- return TimelineExportSerializer(timeline_qs, many=True).data
diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py
index fe1fafec..f4f5326d 100644
--- a/taiga/export_import/service.py
+++ b/taiga/export_import/service.py
@@ -31,7 +31,7 @@ from django.core.files.storage import default_storage
from taiga.base.utils import json
from taiga.projects.history.services import make_key_from_model_object, take_snapshot
-from taiga.timeline.service import build_project_namespace
+from taiga.timeline.service import build_project_namespace, get_project_timeline
from taiga.projects.references import sequences as seq
from taiga.projects.references import models as refs
from taiga.projects.userstories.models import RolePoints
@@ -132,7 +132,22 @@ def render_project(project, outfile, chunk_size = 8190):
value = field.field_to_native(project, field_name)
outfile.write('"{}": {}'.format(field_name, json.dumps(value)))
- outfile.write('}\n')
+ # Generate the timeline
+ outfile.write(',\n"timeline": [\n')
+ first_timeline = True
+ for timeline_item in get_project_timeline(project).iterator():
+ # Avoid writing "," in the last element
+ if not first_timeline:
+ outfile.write(",\n")
+ else:
+ first_timeline = False
+
+ dumped_value = json.dumps(serializers.TimelineExportSerializer(timeline_item).data)
+ outfile.write(dumped_value)
+ outfile.flush()
+ gc.collect()
+
+ outfile.write(']}\n')
def store_project(data):
From c2323c2ad76b3b8da4abc8ce71c72443de01858b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 8 Mar 2016 18:28:30 +0100
Subject: [PATCH 070/105] [i18n] Update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/de/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/en/LC_MESSAGES/django.po | 178 ++++++++++----------
taiga/locale/es/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/fi/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/fr/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/it/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/nl/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/pl/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/ru/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/sv/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/tr/LC_MESSAGES/django.po | 180 +++++++++++----------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 180 +++++++++++----------
14 files changed, 1315 insertions(+), 1203 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 4bdb1457..4b4afcbb 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -66,8 +66,8 @@ msgid "Error on creating new user."
msgstr "Error creant un nou usuari."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Token invàlid"
@@ -183,8 +183,8 @@ msgstr ""
"Puja una imatge vàlida. El fitxer que has pujat no ès una imatge o el fitxer "
"està corrupte."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -340,11 +340,15 @@ msgstr "Error d'integritat per argument invàlid o erroni."
msgid "Precondition error"
msgstr "Precondició errònia."
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr ""
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr ""
@@ -532,21 +536,21 @@ msgstr ""
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr ""
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Conté camps personalitzats invàlids."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -713,7 +717,7 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -733,7 +737,7 @@ msgstr ""
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -771,7 +775,7 @@ msgstr "Comentari"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1143,41 +1147,41 @@ msgstr "Administrar valors de projecte"
msgid "Admin roles"
msgstr "Administrar rols"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Arguments incomplets."
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Format d'image invàlid"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
@@ -1190,7 +1194,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1199,7 +1203,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1221,8 +1225,8 @@ msgstr "Id d'objecte"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1242,7 +1246,7 @@ msgstr "està obsolet "
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1519,27 +1523,27 @@ msgstr "M'agrada"
msgid "Likes"
msgstr "Fans"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "Data estimada d'inici"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "Data estimada de finalització"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "està tancat"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibilitat"
@@ -1564,168 +1568,168 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "text extra d'invitació"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "L'usuari ja es membre del projecte"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "Points per defecte"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "estatus d'història d'usuai per defecte"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "Estatus de tasca per defecte"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "Prioritat per defecte"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "Severitat per defecte"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "Status d'incidència per defecte"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "Tipus d'incidència per defecte"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de fites"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "total de punts d'història"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "activa panell de backlog"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "activa panell de kanban"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "activa panell de wiki"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "activa panell d'incidències"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de videoconferència"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "template de creació"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permisos d'anònims"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permisos d'usuaris"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "es privat"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colors de tags"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Actualitzada data"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2333,47 +2337,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Aquest e-mail ja està en ús"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Rol invàlid per al projecte"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Rols"
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 2b98be80..96010dfd 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -76,8 +76,8 @@ msgid "Error on creating new user."
msgstr "Fehler bei der Erstellung des neuen Benutzers."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Ungültiges Token"
@@ -213,8 +213,8 @@ msgstr ""
"Bitte laden Sie ein gültiges Bild hoch. Die Datei, die Sie hochgeladen "
"haben, ist entweder kein Bild oder defekt."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -372,11 +372,15 @@ msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente"
msgid "Precondition error"
msgstr "Voraussetzungsfehler"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Fehler in Filter Parameter Typen."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
@@ -587,21 +591,21 @@ msgstr "Fehler beim Importieren der Schlagworte"
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Enthält ungültige Benutzerfelder."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Der Name für das Projekt ist doppelt vergeben"
@@ -861,7 +865,7 @@ msgstr "Authentifizierung erforderlich"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -881,7 +885,7 @@ msgstr "Web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -919,7 +923,7 @@ msgstr "Kommentar"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1333,41 +1337,41 @@ msgstr "Administrator Projekt Werte"
msgid "Admin roles"
msgstr "Administrator-Rollen"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Unvollständige Argumente"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ungültiges Bildformat"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Unglültiger Templatename"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Ungültige Templatebeschreibung"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
@@ -1380,7 +1384,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1389,7 +1393,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1411,8 +1415,8 @@ msgstr "Objekt Nr."
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1432,7 +1436,7 @@ msgstr "wurde verworfen"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1713,27 +1717,27 @@ msgstr "Like"
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "Slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "geschätzter Starttermin"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "geschätzter Endtermin"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "ist geschlossen"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "Verfügbarkeit"
@@ -1758,168 +1762,168 @@ msgstr "'{param}' Parameter ist ein Pflichtfeld"
msgid "'project' parameter is mandatory"
msgstr "Der 'project' Parameter ist ein Pflichtfeld"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "E-Mail"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "erstellt am "
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "Token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Einladung Zusatztext "
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "Benutzerreihenfolge"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Der Benutzer ist bereits Mitglied dieses Projekts"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "voreingestellte Punkte"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "voreingesteller User-Story Status "
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "voreingestellter Aufgabenstatus"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "voreingestellte Priorität "
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "voreingestellte Gewichtung "
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "voreingestellter Ticket Status"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "voreingestellter Ticket Typ"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "Mitglieder"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "Meilensteine Gesamt"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "Story Punkte insgesamt"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktives Backlog Panel"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktives Kanban Panel"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktives Wiki Panel"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktives Tickets Panel"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "Videokonferenzsystem"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "Zusatzdaten Videokonferenz"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "Vorlage erstellen"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "Rechte für anonyme Nutzer"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "Rechte für registrierte Nutzer"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "ist privat"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "Tag Farben"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Aktualisierungsdatum"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "Count"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2805,47 +2809,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Die E-Mailadresse ist bereits vergeben"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Ungültige Rolle für dieses Projekt"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Rollen"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index a08f62f6..c97be62f 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-02-19 17:24+0100\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -62,8 +62,8 @@ msgid "Error on creating new user."
msgstr ""
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr ""
@@ -175,8 +175,8 @@ msgid ""
"corrupted image."
msgstr ""
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -332,11 +332,15 @@ msgstr ""
msgid "Precondition error"
msgstr ""
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr ""
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr ""
@@ -521,21 +525,21 @@ msgstr ""
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr ""
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr ""
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr ""
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -702,7 +706,7 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -722,7 +726,7 @@ msgstr ""
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -760,7 +764,7 @@ msgstr ""
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1116,41 +1120,41 @@ msgstr ""
msgid "Admin roles"
msgstr ""
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr ""
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr ""
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr ""
@@ -1163,7 +1167,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1172,7 +1176,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1194,8 +1198,8 @@ msgstr ""
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1215,7 +1219,7 @@ msgstr ""
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1492,27 +1496,27 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr ""
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr ""
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr ""
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr ""
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr ""
@@ -1537,168 +1541,168 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr ""
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr ""
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr ""
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr ""
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr ""
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr ""
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr ""
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr ""
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr ""
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr ""
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr ""
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr ""
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr ""
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr ""
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr ""
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr ""
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr ""
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr ""
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr ""
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr ""
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr ""
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr ""
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr ""
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr ""
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr ""
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2300,47 +2304,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr ""
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr ""
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index 8e9b8f17..f40dcf27 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -13,8 +13,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -70,8 +70,8 @@ msgid "Error on creating new user."
msgstr "Error al crear un nuevo usuario "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Token inválido"
@@ -197,8 +197,8 @@ msgid ""
"corrupted image."
msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -357,11 +357,15 @@ msgstr "Error de integridad por argumentos incorrectos o inválidos"
msgid "Precondition error"
msgstr "Error por incumplimiento de precondición"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Error en los típos de parámetros de filtrado"
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
@@ -571,21 +575,21 @@ msgstr "error importando las etiquetas"
msgid "error importing timelines"
msgstr "error importando los timelines"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" no se ha encontrado en este proyecto"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Contiene attributos personalizados inválidos."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nombre duplicado para el proyecto"
@@ -840,7 +844,7 @@ msgstr "Se requiere autenticación"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -860,7 +864,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -898,7 +902,7 @@ msgstr "comentario"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1310,41 +1314,41 @@ msgstr "Administrar valores de proyecto"
msgid "Admin roles"
msgstr "Administrar roles"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagen no válido"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Nombre de plantilla invalido"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Descripción de plantilla invalida"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
@@ -1357,7 +1361,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1366,7 +1370,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1388,8 +1392,8 @@ msgstr "id de objeto"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1409,7 +1413,7 @@ msgstr "está desactualizado"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1686,27 +1690,27 @@ msgstr "Like"
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "fecha estimada de comienzo"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "fecha estimada de finalización"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "está cerrada"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibilidad"
@@ -1733,168 +1737,168 @@ msgstr "el parámetro '{param}' es obligatório"
msgid "'project' parameter is mandatory"
msgstr "el parámetro 'project' es obligatório"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "creado el"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "texto extra de la invitación"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "orden del usuario"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "El usuario ya es miembro del proyecto"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "puntos por defecto"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "estado de historia por defecto"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "estado de tarea por defecto"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "prioridad por defecto"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "gravedad por defecto"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "estado de petición por defecto"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipo de petición por defecto"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "miembros"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de sprints"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "puntos de historia totales"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "panel de backlog activado"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "panel de kanban activado"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "panel de wiki activo"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "panel de peticiones activo"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de videoconferencia"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "datos extra de videoconferencia"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "creación de plantilla"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permisos de anónimo"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permisos de usuario"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "privado"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "es destacado"
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "está buscando a gente"
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr "nota (buscando a gente)"
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colores de etiquetas"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "fecha y hora de actualización"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "recuento"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr "fans la última semana"
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr "fans el último mes"
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr "fans el último año"
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "actividad la última semana"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "actividad el último mes"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2733,47 +2737,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "La dirección de email ya está en uso."
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Rol inválido para el proyecto"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Roles"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index bc555044..4f15c826 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -67,8 +67,8 @@ msgid "Error on creating new user."
msgstr "Virhe käyttäjän luonnissa."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Väärä tunniste"
@@ -185,8 +185,8 @@ msgstr ""
"Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli "
"vioittunut."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -342,11 +342,15 @@ msgstr "Integrity Error for wrong or invalid arguments"
msgid "Precondition error"
msgstr "Precondition error"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Error in filter params types."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
@@ -557,21 +561,21 @@ msgstr "virhe avainsanojen sisäänlukemisessa"
msgid "error importing timelines"
msgstr "virhe aikajanojen tuonnissa"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" ei löytynyt tästä projektista"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Sisältää vieheellisiä omia kenttiä."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nimi on tuplana projektille"
@@ -823,7 +827,7 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -843,7 +847,7 @@ msgstr ""
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -881,7 +885,7 @@ msgstr "kommentti"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1272,41 +1276,41 @@ msgstr "Hallinnoi projektin arvoja"
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Puutteelliset argumentit"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Väärä kuvaformaatti"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Virheellinen mallipohjan nimi"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Virheellinen mallipohjan kuvaus"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
@@ -1319,7 +1323,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1328,7 +1332,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1350,8 +1354,8 @@ msgstr "objekti ID"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1371,7 +1375,7 @@ msgstr "on poistettu"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1648,27 +1652,27 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "hukka-aika"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "arvioitu alkupvm"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "arvioitu loppupvm"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "on suljettu"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibility"
@@ -1693,168 +1697,168 @@ msgstr "'{param}' parametri on pakollinen"
msgid "'project' parameter is mandatory"
msgstr "'project' parametri on pakollinen"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "sähköposti"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "luo täällä"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "tunniste"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "kutsun lisäteksti"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "käyttäjäjärjestys"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Käyttäjä on jo projektin jäsen"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "oletuspisteet"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "oletus Kt tila"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "oletus tehtävän tila"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "oletus kiireellisyys"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "oletus vakavuus"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "oletus pyynnön tila"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "oletus pyyntö tyyppi"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "jäsenet"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "virstapyväitä yhteensä"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "käyttäjätarinan yhteispisteet"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktiivinen odottavien paneeli"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktiivinen kanban-paneeli"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktiivinen wiki-paneeli"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktiivinen pyyntöpaneeli"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videokokous järjestelmä"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "luo mallipohja"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "vieraan oikeudet"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "käyttäjän oikeudet"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "on yksityinen"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "avainsanojen värit"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "päivityspvm"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2702,47 +2706,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Sähköpostiosoite on jo käytössä"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Virheellinen rooli projektille"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Roolit"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index d9cb55a4..065255a7 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -21,8 +21,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -79,8 +79,8 @@ msgid "Error on creating new user."
msgstr "Erreur à la création du nouvel utilisateur."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Jeton invalide"
@@ -208,8 +208,8 @@ msgstr ""
"Envoyez une image valide. Le fichier que vous avez envoyé n'était pas une "
"image ou était une image corrompue."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -369,11 +369,15 @@ msgstr "Erreur d'intégrité ou arguments invalides"
msgid "Precondition error"
msgstr "Erreur de précondition"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: 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:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
@@ -591,21 +595,21 @@ msgstr "erreur lors de l'importation des mots-clés"
msgid "error importing timelines"
msgstr "erreur lors de l'import des timelines"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" non trouvé dans the projet"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Contient des champs personnalisés non valides."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nom dupliqué pour ce projet"
@@ -839,7 +843,7 @@ msgstr "Authentification requise"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -859,7 +863,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -897,7 +901,7 @@ msgstr "Commentaire"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1277,41 +1281,41 @@ msgstr "Administrer les paramètres du projet"
msgid "Admin roles"
msgstr "Administrer les rôles"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "arguments manquants"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "format de l'image non valide"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Nom de modèle non valide"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Description du modèle non valide"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
@@ -1324,7 +1328,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1333,7 +1337,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1355,8 +1359,8 @@ msgstr "identifiant de l'objet"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1376,7 +1380,7 @@ msgstr "est obsolète"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1653,27 +1657,27 @@ msgstr "Aimer"
msgid "Likes"
msgstr "Aime"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "date de démarrage estimée"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "date de fin estimée"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "est fermé"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibilité"
@@ -1698,168 +1702,168 @@ msgstr "'{param}' paramètre obligatoire"
msgid "'project' parameter is mandatory"
msgstr "'project' paramètre obligatoire"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "Créé le"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "jeton"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Text supplémentaire de l'invitation"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "classement utilisateur"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "L'utilisateur est déjà un membre du projet"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "Points par défaut"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "statut de l'HU par défaut"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "Etat par défaut des tâches"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "Priorité par défaut"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "Sévérité par défaut"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "statut du problème par défaut"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "type de problème par défaut"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total des jalons"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "total des points d'histoire"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "panneau backlog actif"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "panneau kanban actif"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "panneau wiki actif"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "panneau problèmes actif"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "plateforme de vidéoconférence"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "données complémentaires pour la salle de vidéoconférence"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "Modèle de création"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "Permissions anonymes"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "Permission de l'utilisateur"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "est privé"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "est mis en avant"
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "est à la recherche de main d'oeuvre"
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "couleurs des tags"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "date de mise à jour"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "total"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr "fans la semaine dernière"
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr "fans le mois dernier"
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr "fans l'année dernière"
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "activité de la semaine écoulée"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "activité du mois écoulé"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2489,47 +2493,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Adresse email déjà existante"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Rôle non valide pour le projet"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Rôles"
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index 773e3463..e8f67327 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,8 +14,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -73,8 +73,8 @@ msgid "Error on creating new user."
msgstr "Errore nella creazione dell'utente."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Token non valido"
@@ -194,8 +194,8 @@ msgstr ""
"Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine "
"o l'immagine potrebbe essere corrotta. "
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -354,11 +354,15 @@ msgstr "Errore di integrità causato da un argomento invalido o sbagliato"
msgid "Precondition error"
msgstr "Errore di precondizione"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Errore nel filtro del tipo di parametri."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
@@ -582,21 +586,21 @@ msgstr "Errore nell'importazione dei tags"
msgid "error importing timelines"
msgstr "Errore nell'importazione delle timelines"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" non è stato trovato in questo progetto"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Contiene campi personalizzati invalidi."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Il nome del progetto è duplicato"
@@ -910,7 +914,7 @@ msgstr "E' richiesta l'autenticazione"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -930,7 +934,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -968,7 +972,7 @@ msgstr "Commento"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1406,41 +1410,41 @@ msgstr "Valori dell'amministratore del progetto"
msgid "Admin roles"
msgstr "Ruoli dell'amministratore"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argomento non valido"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato dell'immagine non valido"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Il nome del template non è valido"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "La descrizione del template non è valida"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
@@ -1453,7 +1457,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1462,7 +1466,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1484,8 +1488,8 @@ msgstr "ID dell'oggetto"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1505,7 +1509,7 @@ msgstr "non approvato"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1782,27 +1786,27 @@ msgstr "Like"
msgid "Likes"
msgstr "Piaciuto"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "lumaca"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "data stimata di inizio"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "data stimata di fine"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "è concluso"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibilità"
@@ -1828,168 +1832,168 @@ msgstr "il parametro '{param}' è obbligatorio"
msgid "'project' parameter is mandatory"
msgstr "il parametro 'project' è obbligatorio"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "creato a "
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "testo ulteriore per l'invito"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "ordine dell'utente"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "L'utente è già membro del progetto"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "punti predefiniti"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "stati predefiniti per le storie utente"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "stati predefiniti del compito"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "priorità predefinita"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "criticità predefinita"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "stato predefinito del problema"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipologia predefinita del problema"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membri"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "tappe totali"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "punti totali della storia"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "pannello di backlog attivo"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "pannello kanban attivo"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "pannello wiki attivo"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "pannello dei problemi attivo"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema di videoconferenza"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "ulteriori dati di videoconferenza "
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "creazione del template"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permessi anonimi"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permessi dell'utente"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "è privato"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "in vetrina"
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "sta cercando persone"
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr "note sulla ricerca delle persone "
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colori dei tag"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "tempo e data aggiornati"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "conta"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr "fans nella settimana"
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr "fans nel mese"
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr "fans nell'anno"
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "attività nella settimana"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "attività nel mese"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2977,47 +2981,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "L'indirizzo email è già usato"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Ruolo di progetto non valido"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Ruoli"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index aa3f883f..3312f7c7 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -66,8 +66,8 @@ msgid "Error on creating new user."
msgstr "Fout bij het aanmaken van een nieuwe gebruiker."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Ongeldig token"
@@ -194,8 +194,8 @@ msgstr ""
"Upload een geldige afbeelding. Het bestand dat je hebt geuploadet was ofwel "
"een afbeelding ofwel een corrupte afbeelding."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -352,11 +352,15 @@ msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten"
msgid "Precondition error"
msgstr "Preconditie fout"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Fout in filter params types."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
@@ -570,21 +574,21 @@ msgstr "fout bij importeren tags"
msgid "error importing timelines"
msgstr "fout bij importeren tijdlijnen"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" niet gevonden in dit project"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Het bevat ongeldige eigen velden:"
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Naam gedupliceerd voor het project"
@@ -773,7 +777,7 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -793,7 +797,7 @@ msgstr ""
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -831,7 +835,7 @@ msgstr "commentaar"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1206,41 +1210,41 @@ msgstr "Admin project waarden"
msgid "Admin roles"
msgstr "Admin rollen"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Onvolledige argumenten"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ongeldig afbeelding formaat"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Ongeldige template naam"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Ongeldige template omschrijving"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
@@ -1253,7 +1257,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1262,7 +1266,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1284,8 +1288,8 @@ msgstr "object id"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1305,7 +1309,7 @@ msgstr "is verouderd"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1584,27 +1588,27 @@ msgstr "Vind ik leuk"
msgid "Likes"
msgstr "Personen die dit leuk vinden"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "geschatte start datum"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "geschatte datum van afwerking"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "is gesloten"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "beschikbaarheid"
@@ -1629,168 +1633,168 @@ msgstr "'{param}' parameter is verplicht"
msgid "'project' parameter is mandatory"
msgstr "'project' parameter is verplicht"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "aangemaakt op"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "uitnodiging extra text"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "gebruiker volgorde"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "The gebruikers is al lid van het project"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "standaard punten"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "standaard US status"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "default taak status"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "standaard prioriteit"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "standaard ernstniveau"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "standaard issue status"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "standaard issue type"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "leden"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "totaal van de milestones"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "totaal story points"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "actief backlog paneel"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "actief kanban paneel"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "actief wiki paneel"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "actief issues paneel"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videoconference systeem"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "aanmaak template"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonieme toestemmingen"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "gebruikers toestemmingen"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "is privé"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "tag kleuren"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "gewijzigde datum en tijd"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2422,47 +2426,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "E-mail adres is al in gebruik"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Ongeldige rol voor project"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Rollen"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 4be40180..33c704bf 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "Błąd przy tworzeniu użytkownika."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Nieprawidłowy token"
@@ -188,8 +188,8 @@ msgstr ""
"Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub "
"jest uszkodzony."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -347,11 +347,15 @@ msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów"
msgid "Precondition error"
msgstr "Błąd warunków wstępnych"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Błąd w parametrach typów filtrów."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
@@ -571,21 +575,21 @@ msgstr "błąd w trakcie importu tagów"
msgid "error importing timelines"
msgstr "błąd w trakcie importu osi czasu"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" nie odnaleziono w projekcie"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Zawiera niewłaściwe pola niestandardowe."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nazwa projektu zduplikowana"
@@ -841,7 +845,7 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -861,7 +865,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -899,7 +903,7 @@ msgstr "komentarz"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1314,41 +1318,41 @@ msgstr "Administruj wartościami projektu"
msgid "Admin roles"
msgstr "Administruj rolami"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Pola niekompletne"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Niepoprawny format obrazka"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Nieprawidłowa nazwa szablonu"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Nieprawidłowy opis szablonu"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
@@ -1361,7 +1365,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1370,7 +1374,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1392,8 +1396,8 @@ msgstr "id obiektu"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1413,7 +1417,7 @@ msgstr "jest przestarzałe"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1690,27 +1694,27 @@ msgstr ""
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "szacowana data rozpoczecia"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "szacowana data zakończenia"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "jest zamknięte"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "dostępność"
@@ -1735,168 +1739,168 @@ msgstr "'{param}' parametr jest obowiązkowy"
msgid "'project' parameter is mandatory"
msgstr "'project' parametr jest obowiązkowy"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "utwórz na"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "dodatkowy tekst w zaproszeniu"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "kolejność użytkowników"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Użytkownik już jest członkiem tego projektu"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "domyślne punkty"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "domyślny status dla HU"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "domyślny status dla zadania"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "domyślny priorytet"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "domyślna ważność"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "domyślny status dla zgłoszenia"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "domyślny typ dla zgłoszenia"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "członkowie"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "wszystkich kamieni milowych"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "wszystkich punktów "
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktywny panel backlog"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktywny panel Kanban"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktywny panel Wiki"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktywny panel zgłoszeń "
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "system wideokonferencji"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "dodatkowe dane dla wideokonferencji"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "szablon "
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "uprawnienia anonimowych"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "uprawnienia użytkownika"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "jest prywatna"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "kolory tagów"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data aktualizacji"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "ilość"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2759,47 +2763,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Tena adres e-mail jest już w użyciu"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Nieprawidłowa rola w projekcie"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
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 13da7195..56f511c7 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+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"
@@ -76,8 +76,8 @@ msgid "Error on creating new user."
msgstr "Erro ao criar um novo usuário."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Token inválido"
@@ -195,8 +195,8 @@ msgstr ""
"Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou "
"está corrompido."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -354,11 +354,15 @@ msgstr "Erro de Integridade para argumentos inválidos ou errados"
msgid "Precondition error"
msgstr "Erro de pré-condição"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Erro nos tipos de parâmetros do filtro."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
@@ -579,21 +583,21 @@ msgstr "erro importando tags"
msgid "error importing timelines"
msgstr "erro importando linha do tempo"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" não encontrado nesse projeto"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Contém campos personalizados inválidos"
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nome duplicado para o projeto"
@@ -848,7 +852,7 @@ msgstr "Autenticação necessária"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -868,7 +872,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -906,7 +910,7 @@ msgstr "comentário"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1320,41 +1324,41 @@ msgstr "Valores projeto admin"
msgid "Admin roles"
msgstr "Funções Admin"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagem inválida"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Nome de template inválido"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Descrição de template inválida"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
@@ -1367,7 +1371,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1376,7 +1380,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1398,8 +1402,8 @@ msgstr "identidade de objeto"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1419,7 +1423,7 @@ msgstr "está obsoleto"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1696,27 +1700,27 @@ msgstr "Curtir"
msgid "Likes"
msgstr "Curtidas"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slug"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "data de início estimada"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "data de encerramento estimada"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "está fechado"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibilidade"
@@ -1741,168 +1745,168 @@ msgstr "'{param}' parametro é mandatório"
msgid "'project' parameter is mandatory"
msgstr "'project' parametro é mandatório"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "criado em"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "texto extra de convite"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "ordem de usuário"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "O usuário já é membro do projeto"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "pontos padrão"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "status de US padrão"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "status padrão de tarefa"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "prioridade padrão"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "severidade padrão"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "status padrão de caso"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipo padrão de caso"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membros"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de marcos de progresso"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "pontos totais de US"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "painel de backlog ativo"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "painel de kanban ativo"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "painel de wiki ativo"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "painel de casos ativo"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "sistema de vídeo conferência"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "informação extra de vídeo conferência"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "template de criação"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permissão anônima"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permissão de usuário"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "é privado"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "cores de tags"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data de atualização"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "contagem"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2743,47 +2747,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Endereço de e-mail já utilizado"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Função inválida para projeto"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Funções"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index 19e89d04..e23090bc 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -74,8 +74,8 @@ msgid "Error on creating new user."
msgstr "Ошибка при создании нового пользователя."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Неверный токен"
@@ -198,8 +198,8 @@ msgstr ""
"Загрузите корректное изображение. Файл, который вы загрузили - либо не "
"изображение, либо не корректное изображение."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -356,11 +356,15 @@ msgstr "Ошибка целостности из-за неправильных
msgid "Precondition error"
msgstr "Ошибка предусловия"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Ошибка в типах фильтров для параметров."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
@@ -581,21 +585,21 @@ msgstr "ошибка импорта тэгов"
msgid "error importing timelines"
msgstr "ошибка импорта хронологии проекта"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" не найдено в этом проекте"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Содержит неверные специальные поля"
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Уже есть такое имя для проекта"
@@ -848,7 +852,7 @@ msgstr "Необходима аутентификация"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -868,7 +872,7 @@ msgstr "веб"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -906,7 +910,7 @@ msgstr "комментарий"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1321,41 +1325,41 @@ msgstr "Управлять значениями проекта"
msgid "Admin roles"
msgstr "Управлять ролями"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Неправильный формат изображения"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Неверное название шаблона"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Неверное описание шаблона"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
@@ -1368,7 +1372,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1377,7 +1381,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1399,8 +1403,8 @@ msgstr "идентификатор объекта"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1420,7 +1424,7 @@ msgstr "устаревшее"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1701,27 +1705,27 @@ msgstr "Лайк"
msgid "Likes"
msgstr "Лайки"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "ссылочное имя"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "предполагаемая дата начала"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "предполагаемая дата завершения"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "закрыто"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "доступность"
@@ -1748,168 +1752,168 @@ msgstr "параметр '{param}' является обязательным"
msgid "'project' parameter is mandatory"
msgstr "параметр 'project' является обязательным"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "электронная почта"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "создано"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "идентификатор"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "дополнительный текст к приглашению"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "порядок пользователей"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Этот пользователем уже является участником проекта"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "очки по умолчанию"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "статусы ПИ по умолчанию"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "статус задачи по умолчанию"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "приоритет по умолчанию"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "важность по умолчанию"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "статус запроса по умолчанию"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "тип запроса по умолчанию"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "лготип"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "участники"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "общее количество вех"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "очки истории"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "активная панель списка задач"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "активная панель kanban"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "активная wiki-панель"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "панель активных запросов"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "система видеоконференций"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "дополнительные данные системы видеоконференций"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "шаблон для создания"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "права анонимов"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "права пользователя"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "личное"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "цвета тэгов"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "дата и время обновления"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "количество"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "активность за неделю"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "активность за месяц"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2758,47 +2762,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "Этот почтовый адрес уже используется"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Неверная роль для этого проекта"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Роли"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 26bb0fed..4da6b6db 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -65,8 +65,8 @@ msgid "Error on creating new user."
msgstr "Ett fel uppstod når användaren skapades. "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Felaktig förekomst. "
@@ -187,8 +187,8 @@ msgstr ""
"Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild "
"eller en skadad bild."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -346,11 +346,15 @@ msgstr "Integritetsfel för felaktiga eller ogiltiga argument"
msgid "Precondition error"
msgstr "Förutsättningsfel"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Fel i filterparametertyper."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
@@ -555,21 +559,21 @@ msgstr "fel vid importering av taggar"
msgid "error importing timelines"
msgstr "fel vid importering av tidslinje"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" gick inte att hitta för det här projektet"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Felaktigt innehåll. Det måste vara {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Innehåller felaktigt anpassad fält."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Namnet är upprepad för projektet"
@@ -736,7 +740,7 @@ msgstr "Verifiering krävs"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -756,7 +760,7 @@ msgstr "Internet"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -794,7 +798,7 @@ msgstr "kommentera"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1161,41 +1165,41 @@ msgstr "Administrera projektvärden"
msgid "Admin roles"
msgstr "Administratorroller"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Felaktiga argument"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Felaktigt bildformat"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Inget giltigt mallnamn"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Inte giltigt mallbeskrivning"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
@@ -1208,7 +1212,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1217,7 +1221,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1239,8 +1243,8 @@ msgstr "objekt-ID"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1260,7 +1264,7 @@ msgstr "undviks"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1537,27 +1541,27 @@ msgstr "Gillar"
msgid "Likes"
msgstr "Gillar"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "slugg"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "Beräknad startdatum"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "Beräknad slutdato"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "är stängd"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponerar"
@@ -1582,168 +1586,168 @@ msgstr "'{param}' parameter är obligatoriskt"
msgid "'project' parameter is mandatory"
msgstr "'project' parameter är obligatoriskt"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "e-post"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "skapa som"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "textsträng"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Invitation - extra text"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "användarorder"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Användaren är redan medlem i projekt"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "standardpoäng"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "standard US-poäng"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "standard status för uppgift"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "standard prioritet"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "standard allvarsgrad"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "standard status för ärende"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "standard typ för ärende"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "medlemmar"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "totalt antal milstolpar"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "totalt antal historiepoäng"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktivt panel för inkorg"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktiv kanban-panel"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktiv wiki-panel"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktiv panel för ärenden"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videokonferensssystem"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferens - extra data"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "mall skapas"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonyma rättigheter"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "användarbehörigheter"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "är privat"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "färger för taggar"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "uppdaterad dato och tid"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "räkna"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2345,47 +2349,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "E-postadressen är redan använd"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Fel roll for projektet"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "Roller"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index ec95da2a..5e9d463b 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "Geçersiz kupon"
@@ -195,8 +195,8 @@ msgstr ""
"Geçerli bir resim yükleyin. Yüklenen dosya ya bozulmuş bir resim ya da bir "
"resim dosyası değil."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -352,11 +352,15 @@ msgstr "Hatalı ya da geçersiz parametreler için Bütünlük Hatası "
msgid "Precondition error"
msgstr "Ön şart hatası"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "Parametre tipleri filtresinde hata."
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
@@ -565,21 +569,21 @@ msgstr "İçeri aktarılan etiketlerde hata"
msgid "error importing timelines"
msgstr "zaman çizelgesi içeri aktarılırken hata"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" bu projede bulunamadı"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Geçersiz içerik. {\"key\": \"value\",...} şeklinde olması zorunlu"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Geçersiz özel alanlar içeriyor."
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Aynı isimde proje bulunmakta"
@@ -831,7 +835,7 @@ msgstr "Kimlik doğrulama gerekli"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -851,7 +855,7 @@ msgstr "web"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -889,7 +893,7 @@ msgstr "yorum"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1268,41 +1272,41 @@ msgstr "Admin proje değerleri"
msgid "Admin roles"
msgstr "Yönetici rolleri"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Eksik parametreq"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Geçersiz resim biçemi"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "Geçersiz şablon adı"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "Geçersiz şablon tanımı"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
@@ -1315,7 +1319,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1324,7 +1328,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1346,8 +1350,8 @@ msgstr "nesne id"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1367,7 +1371,7 @@ msgstr "kaldırıldı"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1644,27 +1648,27 @@ msgstr "Beğen"
msgid "Likes"
msgstr "Beğeniler"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "satır"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "yaklaşık başlama tarihi"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "yaklaşık bitiş tarihi"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "kapatılmış"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "taşınabilirlik"
@@ -1689,168 +1693,168 @@ msgstr "'{param}' parametresi zorunlu"
msgid "'project' parameter is mandatory"
msgstr "'proje' parametresi zorunlu"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "e-posta"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "kupon"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Davetiye ekstra metni"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "kullanıcı sırası"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Kullanıcı zaten projenin üyesi"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "varsayılan puanlar"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "varsayılan KH durumu"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "varsayılan görev durumu"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "varsayılan öncelik"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "varsayılan önem derecesi"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "varsayılan talep durumu"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "varsayılan talep tipi"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "üyeler"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "aşamaların toplamı"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "toplam hikaye puanı"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "aktif birikmiş iler paneli"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "aktif kanban paneli"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "aktif wiki paneli"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "aktif talep paneli"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "video konferans sistemi"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferans ekstra verisi"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "oluşturma şablonu"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonim izinler"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "kullanıcı izinleri"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "gizli"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "vitrinde"
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "insan arıyor"
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "etiket renkleri"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "yükleme tarih-saati"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "sayı"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr "geçen hafta fanları"
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr "geçen ayın fanları"
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr "geçen yılın fanları"
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "geçen haftanın aktiviteleri"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "geçen ayın aktiviteleri"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2518,47 +2522,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "E-posta adresi önceden alınmış"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "Proje için geçersiz rol"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
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 101df4e7..590b9978 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-02-19 17:24+0100\n"
-"PO-Revision-Date: 2016-02-19 16:24+0000\n"
+"POT-Creation-Date: 2016-03-08 18:27+0100\n"
+"PO-Revision-Date: 2016-03-08 17:28+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"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "無法創建新使用者"
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:368
-#: taiga/projects/api.py:390
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
+#: taiga/projects/api.py:392
msgid "Invalid token"
msgstr "無效的代碼 "
@@ -181,8 +181,8 @@ msgid ""
"corrupted image."
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:206
-#: taiga/hooks/api.py:68 taiga/projects/api.py:620
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:623
#: taiga/projects/issues/api.py:237 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
@@ -338,11 +338,15 @@ msgstr "因錯誤或無效參數,一致性出錯"
msgid "Precondition error"
msgstr "前提出錯"
-#: taiga/base/filters.py:78 taiga/base/filters.py:443
+#: taiga/base/exceptions.py:217
+msgid "Not enough slots for project."
+msgstr ""
+
+#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
msgstr "過濾參數類型出錯"
-#: taiga/base/filters.py:132 taiga/base/filters.py:231
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
#: taiga/projects/filters.py:58
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
@@ -562,21 +566,21 @@ msgstr "滙入標籤出錯"
msgid "error importing timelines"
msgstr "滙入時間軸出錯"
-#: taiga/export_import/serializers.py:178
+#: taiga/export_import/serializers.py:177
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" 無法在此專案中找到"
-#: taiga/export_import/serializers.py:443
+#: taiga/export_import/serializers.py:442
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "無效內容。必須為 {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:458
+#: taiga/export_import/serializers.py:457
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "包括無效慣例欄位"
-#: taiga/export_import/serializers.py:528
+#: taiga/export_import/serializers.py:527
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "專案的名稱被複製了"
@@ -828,7 +832,7 @@ msgstr "要求取得授權"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:145
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
#: taiga/projects/models.py:472 taiga/projects/models.py:511
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
@@ -848,7 +852,7 @@ msgstr "網頁"
#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
#: taiga/projects/custom_attributes/models.py:36
#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:149
+#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
@@ -886,7 +890,7 @@ msgstr "評論"
#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
#: taiga/projects/custom_attributes/models.py:45
#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:156
+#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88
#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
@@ -1294,41 +1298,41 @@ msgstr "管理員專案數值"
msgid "Admin roles"
msgstr "管理員角色"
-#: taiga/projects/api.py:162 taiga/users/api.py:220
+#: taiga/projects/api.py:164 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "不完整參數"
-#: taiga/projects/api.py:166 taiga/users/api.py:225
+#: taiga/projects/api.py:168 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "無效的圖片檔案"
-#: taiga/projects/api.py:227
+#: taiga/projects/api.py:229
msgid "Not valid template name"
msgstr "非有效樣板名稱 "
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:232
msgid "Not valid template description"
msgstr "無效樣板描述"
-#: taiga/projects/api.py:345
+#: taiga/projects/api.py:347
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:352
+#: taiga/projects/api.py:354
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:358
+#: taiga/projects/api.py:360
msgid "The user must be an admin member of the project"
msgstr ""
-#: taiga/projects/api.py:658 taiga/projects/serializers.py:192
+#: taiga/projects/api.py:662
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:697
+#: taiga/projects/api.py:702
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
@@ -1341,7 +1345,7 @@ 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:42 taiga/projects/models.py:161
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162
#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38
#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36
#: taiga/userstorage/models.py:26
@@ -1350,7 +1354,7 @@ 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:44
+#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
#: taiga/projects/models.py:460 taiga/projects/models.py:486
#: taiga/projects/models.py:517 taiga/projects/models.py:546
#: taiga/projects/models.py:579 taiga/projects/models.py:602
@@ -1372,8 +1376,8 @@ msgstr "物件ID"
#: taiga/projects/attachments/models.py:50
#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51
-#: taiga/projects/models.py:159 taiga/projects/models.py:686
+#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
+#: taiga/projects/models.py:160 taiga/projects/models.py:686
#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
msgid "modified date"
@@ -1393,7 +1397,7 @@ msgstr "棄用"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:476
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
@@ -1670,27 +1674,27 @@ msgstr "喜歡"
msgid "Likes"
msgstr "喜歡"
-#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:147
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
msgid "slug"
msgstr "代稱"
-#: taiga/projects/milestones/models.py:45
+#: taiga/projects/milestones/models.py:46
msgid "estimated start date"
msgstr "预計開始日期"
-#: taiga/projects/milestones/models.py:46
+#: taiga/projects/milestones/models.py:47
msgid "estimated finish date"
msgstr "預計完成日期"
-#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:478
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
#: taiga/projects/models.py:542 taiga/projects/models.py:625
msgid "is closed"
msgstr "被關閉"
-#: taiga/projects/milestones/models.py:55
+#: taiga/projects/milestones/models.py:56
msgid "disponibility"
msgstr "disponibility"
@@ -1715,168 +1719,168 @@ msgstr "'{param}' 參數為必要"
msgid "'project' parameter is mandatory"
msgstr "'project'參數為必要"
-#: taiga/projects/models.py:77
+#: taiga/projects/models.py:78
msgid "email"
msgstr "電子郵件"
-#: taiga/projects/models.py:79
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "創建於"
-#: taiga/projects/models.py:81 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:117
msgid "token"
msgstr "代號"
-#: taiga/projects/models.py:87
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "額外文案邀請"
-#: taiga/projects/models.py:90
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "使用者次序"
-#: taiga/projects/models.py:100
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "使用者已是專案成員"
-#: taiga/projects/models.py:115
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "預設點數"
-#: taiga/projects/models.py:119
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "預設使用者故事狀態"
-#: taiga/projects/models.py:123
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "預設任務狀態"
-#: taiga/projects/models.py:126
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "預設優先性"
-#: taiga/projects/models.py:129
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "預設嚴重性"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "預設問題狀態"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "預設議題類型"
-#: taiga/projects/models.py:153
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "圖標"
-#: taiga/projects/models.py:163
+#: taiga/projects/models.py:164
msgid "members"
msgstr "成員"
-#: taiga/projects/models.py:166
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "全部里程碑"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "全部故事點數"
-#: taiga/projects/models.py:170 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "活躍的待辦任務優先表面板"
-#: taiga/projects/models.py:172 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "活躍的看板式面板"
-#: taiga/projects/models.py:174 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "活躍的維基面板"
-#: taiga/projects/models.py:176 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "活躍的問題面板"
-#: taiga/projects/models.py:179 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "視訊會議系統"
-#: taiga/projects/models.py:181 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "視訊會議額外資料"
-#: taiga/projects/models.py:186
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "創建模版"
-#: taiga/projects/models.py:190
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "匿名權限"
-#: taiga/projects/models.py:194
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "使用者權限"
-#: taiga/projects/models.py:197
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "私密"
-#: taiga/projects/models.py:200
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr " 受矚目的"
-#: taiga/projects/models.py:203
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "正在找人"
-#: taiga/projects/models.py:205
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:217
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "標籤顏色"
-#: taiga/projects/models.py:220
+#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:224 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "更新日期時間"
-#: taiga/projects/models.py:227 taiga/projects/models.py:239
+#: taiga/projects/models.py:228 taiga/projects/models.py:240
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "數量"
-#: taiga/projects/models.py:230
+#: taiga/projects/models.py:231
msgid "fans last week"
msgstr "上週粉絲"
-#: taiga/projects/models.py:233
+#: taiga/projects/models.py:234
msgid "fans last month"
msgstr "上個月粉絲"
-#: taiga/projects/models.py:236
+#: taiga/projects/models.py:237
msgid "fans last year"
msgstr "去年粉絲"
-#: taiga/projects/models.py:242
+#: taiga/projects/models.py:243
msgid "activity last week"
msgstr "上週活躍成員"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:246
msgid "activity last month"
msgstr "上月活躍成員"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:249
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:252
+#: taiga/projects/models.py:253
msgid "blocked code"
msgstr ""
@@ -2728,47 +2732,51 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:168
+#: taiga/projects/serializers.py:173
msgid "Email address is already taken"
msgstr "電子郵件已使用"
-#: taiga/projects/serializers.py:180
+#: taiga/projects/serializers.py:185
msgid "Invalid role for the project"
msgstr "專案無效的角色"
-#: taiga/projects/serializers.py:365
+#: taiga/projects/serializers.py:196
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:377
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:366
+#: taiga/projects/serializers.py:378
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:367
+#: taiga/projects/serializers.py:379
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:368
+#: taiga/projects/serializers.py:380
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:369
+#: taiga/projects/serializers.py:381
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:370
+#: taiga/projects/serializers.py:382
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:371
+#: taiga/projects/serializers.py:383
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:372
+#: taiga/projects/serializers.py:384
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:373
+#: taiga/projects/serializers.py:385
msgid "Roles"
msgstr "角色"
From a9c8989f024a4bf0b8a4b7044f78a1ba4284a902 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 8 Mar 2016 21:12:44 +0100
Subject: [PATCH 071/105] Removing unnecesary flush and gc collects from export
task
---
taiga/export_import/service.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py
index f4f5326d..9a739604 100644
--- a/taiga/export_import/service.py
+++ b/taiga/export_import/service.py
@@ -144,8 +144,6 @@ def render_project(project, outfile, chunk_size = 8190):
dumped_value = json.dumps(serializers.TimelineExportSerializer(timeline_item).data)
outfile.write(dumped_value)
- outfile.flush()
- gc.collect()
outfile.write(']}\n')
From 7dcf9c53adf3b0726ac311e089176e7dcb55045c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 9 Mar 2016 18:01:59 +0100
Subject: [PATCH 072/105] :burn: :burn:
---
taiga/projects/milestones/models.py | 51 ++---------------------------
1 file changed, 2 insertions(+), 49 deletions(-)
diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py
index 1e30d858..bf7bb469 100644
--- a/taiga/projects/milestones/models.py
+++ b/taiga/projects/milestones/models.py
@@ -89,7 +89,8 @@ class Milestone(WatchedModelMixin, models.Model):
@cached_property
def cached_user_stories(self):
- return self.user_stories.prefetch_related("role_points", "role_points__points").annotate(num_tasks=Count("tasks"))
+ return (self.user_stories.prefetch_related("role_points", "role_points__points")
+ .annotate(num_tasks=Count("tasks")))
def _get_user_stories_points(self, user_stories):
role_points = [us.role_points.all() for us in user_stories]
@@ -109,54 +110,6 @@ class Milestone(WatchedModelMixin, models.Model):
[us for us in self.cached_user_stories if us.is_closed]
)
- def _get_increment_points(self):
- if hasattr(self, "_increments"):
- return self._increments
-
- self._increments = {
- "client_increment": {},
- "team_increment": {},
- "shared_increment": {},
- }
- user_stories = UserStory.objects.none()
- if self.estimated_start and self.estimated_finish:
- user_stories = filter(
- lambda x: x.created_date.date() >= self.estimated_start and x.created_date.date() < self.estimated_finish,
- self.project.user_stories.all()
- )
- self._increments['client_increment'] = self._get_user_stories_points(
- [us for us in user_stories if us.client_requirement is True and us.team_requirement is False]
- )
- self._increments['team_increment'] = self._get_user_stories_points(
- [us for us in user_stories if us.client_requirement is False and us.team_requirement is True]
- )
- self._increments['shared_increment'] = self._get_user_stories_points(
- [us for us in user_stories if us.client_requirement is True and us.team_requirement is True]
- )
- return self._increments
-
-
- @property
- def client_increment_points(self):
- self._get_increment_points()
- client_increment = self._get_increment_points()["client_increment"]
- shared_increment = {
- key: value/2 for key, value in self._get_increment_points()["shared_increment"].items()
- }
- return dict_sum(client_increment, shared_increment)
-
- @property
- def team_increment_points(self):
- team_increment = self._get_increment_points()["team_increment"]
- shared_increment = {
- key: value/2 for key, value in self._get_increment_points()["shared_increment"].items()
- }
- return dict_sum(team_increment, shared_increment)
-
- @property
- def shared_increment_points(self):
- return self._get_increment_points()["shared_increment"]
-
def total_closed_points_by_date(self, date):
# Milestone instance will keep a cache of the total closed points by date
if self._total_closed_points_by_date is None:
From 058948ee733fc9a67f9cddfec73a0b3d4bddd401 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 14 Mar 2016 09:24:19 +0100
Subject: [PATCH 073/105] Adding extra reason to block project
---
taiga/projects/choices.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py
index 9bebce00..9baa1218 100644
--- a/taiga/projects/choices.py
+++ b/taiga/projects/choices.py
@@ -25,9 +25,11 @@ VIDEOCONFERENCES_CHOICES = (
("talky", _("Talky")),
)
+BLOCKED_BY_NONPAYMENT = "blocked-by-nonpayment"
BLOCKED_BY_STAFF = "blocked-by-staff"
BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving"
BLOCKING_CODES = [
+ (BLOCKED_BY_NONPAYMENT, _("This project was blocked by nonpayment")),
(BLOCKED_BY_STAFF, _("This project was blocked by staff")),
(BLOCKED_BY_OWNER_LEAVING, _("This project was blocked because the owner left"))
]
From 765a31f5cbee8bd64fdeb8db5c8a9e9a392c2bc1 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 14 Mar 2016 14:48:10 +0100
Subject: [PATCH 074/105] Fixing burndown graph
---
taiga/projects/services/stats.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py
index f0d70f1c..6e12ff06 100644
--- a/taiga/projects/services/stats.py
+++ b/taiga/projects/services/stats.py
@@ -196,8 +196,10 @@ def _get_milestones_stats_for_backlog(project, milestones):
else:
milestone_name = _("Future sprint")
- team_increment = current_team_increment + project._future_team_increment,
- client_increment = current_client_increment + project._future_client_increment,
+ current_team_increment += project._future_team_increment
+ current_client_increment += project._future_client_increment
+ team_increment = current_team_increment
+ client_increment = current_client_increment
current_evolution = None
milestones_stats.append({
@@ -216,8 +218,8 @@ def _get_milestones_stats_for_backlog(project, milestones):
'name': _('Project End'),
'optimal': optimal_points,
'evolution': evolution,
- 'team-increment': team_increment,
- 'client-increment': client_increment,
+ 'team-increment': current_team_increment,
+ 'client-increment': current_client_increment,
})
return milestones_stats
From 97a45c7095d3cdbd74797096ecc0f03445712af0 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 15 Mar 2016 10:46:28 +0100
Subject: [PATCH 075/105] Adding transfer_validate_token endpoint to projects
API
---
taiga/projects/api.py | 8 ++
taiga/projects/permissions.py | 1 +
taiga/projects/services/transfer.py | 10 +-
tests/integration/test_projects.py | 140 ++++++++++++++++++++++++++++
4 files changed, 154 insertions(+), 5 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index ba72c3e7..41c5268e 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -330,6 +330,14 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors))
+ @detail_route(methods=["POST"])
+ def transfer_validate_token(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "transfer_validate_token", project)
+ token = request.DATA.get('token', None)
+ services.transfer.validate_project_transfer_token(token, project, request.user)
+ return response.Ok()
+
@detail_route(methods=["POST"])
def transfer_request(self, request, pk=None):
project = self.get_object()
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index aecf8c3e..0db55117 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -77,6 +77,7 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
+ transfer_validate_token_perms = IsProjectAdmin()
transfer_request_perms = IsProjectAdmin()
transfer_start_perms = IsMainOwner()
transfer_reject_perms = IsProjectAdmin()
diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py
index 4ee456e2..fcd73ef0 100644
--- a/taiga/projects/services/transfer.py
+++ b/taiga/projects/services/transfer.py
@@ -54,10 +54,10 @@ def start_project_transfer(project, user, reason):
email.send()
-def _validate_token(token, project_token, user_id):
+def validate_project_transfer_token(token, project, user):
signer = signing.TimestampSigner()
- if project_token != token:
+ if project.transfer_token != token:
raise exc.WrongArguments(_("Token is invalid"))
try:
@@ -67,12 +67,12 @@ def _validate_token(token, project_token, user_id):
except signing.BadSignature:
raise exc.WrongArguments(_("Token is invalid"))
- if str(value) != str(user_id):
+ if str(value) != str(user.id):
raise exc.WrongArguments(_("Token is invalid"))
def reject_project_transfer(project, user, token, reason):
- _validate_token(token, project.transfer_token, user.id)
+ validate_project_transfer_token(token, project, user)
project.transfer_token = None
project.save()
@@ -88,7 +88,7 @@ def reject_project_transfer(project, user, token, reason):
def accept_project_transfer(project, user, token, reason):
- _validate_token(token, project.transfer_token, user.id)
+ validate_project_transfer_token(token, project, user)
old_owner = project.owner
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 18219055..0065a2a0 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -797,6 +797,7 @@ def test_transfer_request_from_admin_member(client):
assert response.status_code == 200
assert len(mail.outbox) == 1
+
def test_project_transfer_start_to_not_a_membership(client):
user_from = f.UserFactory.create()
project = f.create_project(owner=user_from)
@@ -1253,3 +1254,142 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_
project = Project.objects.get(pk=project.pk)
assert project.owner.id == user_to.id
assert project.transfer_token is None
+
+
+def test_project_transfer_validate_token_from_admin_member_without_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {}
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+
+
+def test_project_transfer_validate_token_from_not_admin_member(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=False)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 403
+
+
+def test_project_transfer_validate_token_from_admin_member_with_invalid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ project = f.create_project(owner=user_from, transfer_token="invalid-token")
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": "invalid-token",
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+
+
+def test_project_transfer_validate_token_from_admin_member_with_other_user_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ other_user = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(other_user.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token is invalid" == response.data["_error_message"]
+
+
+def test_project_transfer_validate_token_from_admin_member_with_expired_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = ExpiredSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "Token has expired" == response.data["_error_message"]
+
+
+
+def test_project_transfer_validate_token_from_admin_member_with_valid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_private_projects=1)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.MembershipFactory(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
From 13c8c6c42d8464463651a34107fbedf205d1c737 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 16 Mar 2016 11:03:07 +0100
Subject: [PATCH 076/105] Change max_members_*_projects to
max_memberships_*_projects
---
settings/common.py | 4 ++--
taiga/users/admin.py | 4 ++--
.../users/migrations/0016_auto_20160204_1050.py | 8 ++++----
taiga/users/models.py | 16 ++++++++--------
taiga/users/serializers.py | 6 +++---
taiga/users/services.py | 8 ++++----
tests/integration/test_importer_api.py | 12 ++++++------
tests/integration/test_memberships.py | 16 ++++++++--------
tests/integration/test_projects.py | 4 ++--
9 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/settings/common.py b/settings/common.py
index c871c54c..568be60c 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -527,8 +527,8 @@ EXTRA_BLOCKING_CODES = []
MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
-MAX_MEMBERS_PRIVATE_PROJECTS = None # None == no limit
-MAX_MEMBERS_PUBLIC_PROJECTS = None # None == no limit
+MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit
+MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit
from .sr import *
diff --git a/taiga/users/admin.py b/taiga/users/admin.py
index 76bc701d..9d4c9815 100644
--- a/taiga/users/admin.py
+++ b/taiga/users/admin.py
@@ -52,8 +52,8 @@ class UserAdmin(DjangoUserAdmin):
(_("Extra info"), {"fields": ("color", "lang", "timezone", "token", "colorize_tags",
"email_token", "new_email")}),
(_("Permissions"), {"fields": ("is_active", "is_superuser")}),
- (_("Restrictions"), {"fields": (("max_private_projects", "max_members_private_projects"),
- ("max_public_projects", "max_members_public_projects"))}),
+ (_("Restrictions"), {"fields": (("max_private_projects", "max_memberships_private_projects"),
+ ("max_public_projects", "max_memberships_public_projects"))}),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
form = UserChangeForm
diff --git a/taiga/users/migrations/0016_auto_20160204_1050.py b/taiga/users/migrations/0016_auto_20160204_1050.py
index d148f56a..244fcd00 100644
--- a/taiga/users/migrations/0016_auto_20160204_1050.py
+++ b/taiga/users/migrations/0016_auto_20160204_1050.py
@@ -14,12 +14,12 @@ class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='user',
- name='max_members_private_projects',
- field=models.IntegerField(default=settings.MAX_MEMBERS_PRIVATE_PROJECTS, blank=True, verbose_name='max number of memberships for each owned private project', null=True),
+ name='max_memberships_private_projects',
+ field=models.IntegerField(default=settings.MAX_MEMBERSHIPS_PRIVATE_PROJECTS, blank=True, verbose_name='max number of memberships for each owned private project', null=True),
),
migrations.AddField(
model_name='user',
- name='max_members_public_projects',
- field=models.IntegerField(default=settings.MAX_MEMBERS_PUBLIC_PROJECTS, blank=True, verbose_name='max number of memberships for each owned public project', null=True),
+ name='max_memberships_public_projects',
+ field=models.IntegerField(default=settings.MAX_MEMBERSHIPS_PUBLIC_PROJECTS, blank=True, verbose_name='max number of memberships for each owned public project', null=True),
),
]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index b4d960c2..054167cc 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -130,14 +130,14 @@ class User(AbstractBaseUser, PermissionsMixin):
max_public_projects = models.IntegerField(null=True, blank=True,
default=settings.MAX_PUBLIC_PROJECTS_PER_USER,
verbose_name=_("max number of public projects owned"))
- max_members_private_projects = models.IntegerField(null=True, blank=True,
- default=settings.MAX_MEMBERS_PRIVATE_PROJECTS,
- verbose_name=_("max number of memberships for "
- "each owned private project"))
- max_members_public_projects = models.IntegerField(null=True, blank=True,
- default=settings.MAX_MEMBERS_PUBLIC_PROJECTS,
- verbose_name=_("max number of memberships for "
- "each owned public project"))
+ max_memberships_private_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_MEMBERSHIPS_PRIVATE_PROJECTS,
+ verbose_name=_("max number of memberships for "
+ "each owned private project"))
+ max_memberships_public_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_MEMBERSHIPS_PUBLIC_PROJECTS,
+ verbose_name=_("max number of memberships for "
+ "each owned public project"))
_cached_memberships = None
_cached_liked_ids = None
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index de95324c..d36ba768 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -115,13 +115,13 @@ class UserAdminSerializer(UserSerializer):
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
"big_photo",
"max_private_projects", "max_public_projects",
- "max_members_private_projects", "max_members_public_projects",
+ "max_memberships_private_projects", "max_memberships_public_projects",
"total_private_projects", "total_public_projects")
read_only_fields = ("id", "email",
"max_private_projects", "max_public_projects",
- "max_members_private_projects",
- "max_members_public_projects")
+ "max_memberships_private_projects",
+ "max_memberships_public_projects")
def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count()
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 64cd5087..e13355ad 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -601,14 +601,14 @@ def _has_available_slot_for_project_members(user, project, members):
current_memberships = project.memberships.count()
if project.is_private:
- if user.max_members_private_projects is None:
+ if user.max_memberships_private_projects is None:
return (True, None)
- elif current_memberships + members <= user.max_members_private_projects:
+ elif current_memberships + members <= user.max_memberships_private_projects:
return (True, None)
return (False, _("You have reached the limit of memberships for private projects"))
else:
- if user.max_members_public_projects is None:
+ if user.max_memberships_public_projects is None:
return (True, None)
- elif current_memberships + members <= user.max_members_public_projects:
+ elif current_memberships + members <= user.max_memberships_public_projects:
return (True, None)
return (False, _("You have reached the limit of memberships for public projects"))
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 90fa70dd..9d4e8d0b 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -1031,7 +1031,7 @@ def test_dict_to_project_with_no_projects_slots_available(client):
def test_dict_to_project_with_no_members_private_project_slots_available(client):
- user = f.UserFactory.create(max_members_private_projects=2)
+ user = f.UserFactory.create(max_memberships_private_projects=2)
data = {
"slug": "valid-project",
@@ -1066,7 +1066,7 @@ def test_dict_to_project_with_no_members_private_project_slots_available(client)
def test_dict_to_project_with_no_members_public_project_slots_available(client):
- user = f.UserFactory.create(max_members_public_projects=2)
+ user = f.UserFactory.create(max_memberships_public_projects=2)
data = {
"slug": "valid-project",
@@ -1281,7 +1281,7 @@ def test_valid_dump_import_without_enough_private_projects_slots(client):
def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client):
- user = f.UserFactory.create(max_members_private_projects=5)
+ user = f.UserFactory.create(max_memberships_private_projects=5)
client.login(user)
url = reverse("importer-load-dump")
@@ -1328,7 +1328,7 @@ def test_valid_dump_import_without_enough_membership_private_project_slots_one_p
def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client):
- user = f.UserFactory.create(max_members_public_projects=5)
+ user = f.UserFactory.create(max_memberships_public_projects=5)
client.login(user)
url = reverse("importer-load-dump")
@@ -1377,7 +1377,7 @@ def test_valid_dump_import_without_enough_membership_public_project_slots_one_pr
def test_valid_dump_import_with_enough_membership_private_project_slots_multiple_projects(client, settings):
settings.CELERY_ENABLED = False
- user = f.UserFactory.create(max_members_private_projects=10)
+ user = f.UserFactory.create(max_memberships_private_projects=10)
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project)
f.MembershipFactory.create(project=project)
@@ -1433,7 +1433,7 @@ def test_valid_dump_import_with_enough_membership_private_project_slots_multiple
def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_projects(client, settings):
settings.CELERY_ENABLED = False
- user = f.UserFactory.create(max_members_public_projects=10)
+ user = f.UserFactory.create(max_memberships_public_projects=10)
project = f.ProjectFactory.create(owner=user)
f.MembershipFactory.create(project=project)
f.MembershipFactory.create(project=project)
diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py
index 6919cb4b..a408f838 100644
--- a/tests/integration/test_memberships.py
+++ b/tests/integration/test_memberships.py
@@ -54,7 +54,7 @@ def test_api_create_bulk_members(client):
def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client):
- user = f.UserFactory.create(max_members_private_projects=3)
+ user = f.UserFactory.create(max_memberships_private_projects=3)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -78,7 +78,7 @@ def test_api_create_bulk_members_without_enough_memberships_private_project_slot
def test_api_create_bulk_members_with_enough_memberships_private_project_slots_multiple_projects(client):
- user = f.UserFactory.create(max_members_private_projects=6)
+ user = f.UserFactory.create(max_memberships_private_projects=6)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -107,7 +107,7 @@ def test_api_create_bulk_members_with_enough_memberships_private_project_slots_m
def test_api_create_bulk_members_without_enough_memberships_public_project_slots_one_project(client):
- user = f.UserFactory.create(max_members_public_projects=3)
+ user = f.UserFactory.create(max_memberships_public_projects=3)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -131,7 +131,7 @@ def test_api_create_bulk_members_without_enough_memberships_public_project_slots
def test_api_create_bulk_members_with_enough_memberships_public_project_slots_multiple_projects(client):
- user = f.UserFactory.create(max_members_public_projects=6)
+ user = f.UserFactory.create(max_memberships_public_projects=6)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -269,7 +269,7 @@ def test_api_create_membership(client):
def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client):
- user = f.UserFactory.create(max_members_private_projects=1)
+ user = f.UserFactory.create(max_memberships_private_projects=1)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -284,7 +284,7 @@ def test_api_create_membership_without_enough_memberships_private_project_slots_
def test_api_create_membership_with_enough_memberships_private_project_slots_multiple_projects(client):
- user = f.UserFactory.create(max_members_private_projects=5)
+ user = f.UserFactory.create(max_memberships_private_projects=5)
project = f.ProjectFactory(owner=user, is_private=True)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -304,7 +304,7 @@ def test_api_create_membership_with_enough_memberships_private_project_slots_mul
def test_api_create_membership_without_enough_memberships_public_project_slots_one_projects(client):
- user = f.UserFactory.create(max_members_public_projects=1)
+ user = f.UserFactory.create(max_memberships_public_projects=1)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
@@ -319,7 +319,7 @@ def test_api_create_membership_without_enough_memberships_public_project_slots_o
def test_api_create_membership_with_enough_memberships_public_project_slots_multiple_projects(client):
- user = f.UserFactory.create(max_members_public_projects=5)
+ user = f.UserFactory.create(max_memberships_public_projects=5)
project = f.ProjectFactory(owner=user, is_private=False)
role = f.RoleFactory(project=project, name="Test")
f.MembershipFactory(project=project, user=user, is_admin=True)
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 0065a2a0..e8db7810 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1161,7 +1161,7 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_public_project_slots(client):
user_from = f.UserFactory.create()
- user_to = f.UserFactory.create(max_members_public_projects=5)
+ user_to = f.UserFactory.create(max_memberships_public_projects=5)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
@@ -1195,7 +1195,7 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_private_project_slots(client):
user_from = f.UserFactory.create()
- user_to = f.UserFactory.create(max_members_private_projects=5)
+ user_to = f.UserFactory.create(max_memberships_private_projects=5)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
From f502a538c266ce800f228b099a843db19a1b39ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 16 Mar 2016 11:00:12 +0100
Subject: [PATCH 077/105] Add 'max_memberships', 'total_memberships' and
'can_is_private_be_updated' to the project serializer
---
taiga/projects/api.py | 1 +
taiga/projects/serializers.py | 13 ++
taiga/projects/services/__init__.py | 2 +
taiga/projects/services/members.py | 49 +++++++
tests/integration/test_projects.py | 192 ++++++++++++++++++++++++++++
5 files changed, 257 insertions(+)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 41c5268e..43388afa 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -109,6 +109,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
+ qs = qs.prefetch_related("memberships")
qs = qs.prefetch_related(Prefetch("notify_policies",
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index e892f93e..28e3593c 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -352,11 +352,24 @@ class ProjectDetailSerializer(ProjectSerializer):
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
+ max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
+ total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
+ can_is_private_be_updated = serializers.SerializerMethodField(method_name="get_can_is_private_be_updated")
+
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
+ def get_max_memberships(self, obj):
+ return services.get_max_memberships_for_project(obj)
+
+ def get_total_memberships(self, obj):
+ return services.get_total_project_memberships(obj)
+
+ def get_can_is_private_be_updated(self, obj):
+ return services.check_if_project_privacity_can_be_changed(obj)
+
######################################################
## Liked
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index c50e8386..6227cc54 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -38,6 +38,8 @@ from .logo import get_logo_big_thumbnail_url
from .members import create_members_in_bulk
from .members import get_members_from_bulk
from .members import remove_user_from_project, project_has_valid_admins, can_user_leave_project
+from .members import get_max_memberships_for_project, get_total_project_memberships
+from .members import check_if_project_privacity_can_be_changed
from .modules_config import get_modules_config
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index 4d6ed0ec..a141f8e7 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -60,3 +60,52 @@ def can_user_leave_project(user, project):
return False
return True
+
+
+def get_max_memberships_for_project(project):
+ """Return tha maximun of membersh for a concrete project.
+
+ :param project: A project object.
+
+ :return: a number or null.
+ """
+ if project.is_private:
+ return project.owner.max_memberships_private_projects
+ return project.owner.max_memberships_public_projects
+
+
+def get_total_project_memberships(project):
+ """Return tha total of memberships of a project (members and unaccepted invitations).
+
+ :param project: A project object.
+
+ :return: a number.
+ """
+ return project.memberships.count()
+
+
+def check_if_project_privacity_can_be_changed(project):
+ """Return if the project privacity can be changed from private to public or viceversa.
+
+ :param project: A project object.
+
+ :return: True if it can be changed or False if can't.
+ """
+ if project.is_private:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ max_projects = project.owner.max_public_projects
+ max_memberships = project.owner.max_memberships_public_projects
+ else:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ max_projects = project.owner.max_private_projects
+ max_memberships = project.owner.max_memberships_private_projects
+
+ if max_projects is not None and current_projects >= max_projects:
+ return False
+
+ current_memberships = project.memberships.count()
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return False
+
+ return True
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index e8db7810..77592abc 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1393,3 +1393,195 @@ def test_project_transfer_validate_token_from_admin_member_with_valid_token(clie
response = client.json.post(url, json.dumps(data))
assert response.status_code == 200
+
+
+####################################################################################
+# Test taiga.projects.services.members.check_if_project_privacity_can_be_changed
+####################################################################################
+
+from taiga.projects.services import check_if_project_privacity_can_be_changed
+
+# private to public
+
+def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot_and_too_much_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 0
+ project.owner.max_memberships_public_projects = 3
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 0
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_private_project_cant_be_public_because_too_much_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = 3
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_private_project_can_be_public_because_owner_has_enought_slot_and_project_has_enought_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_private_project_can_be_public_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = None
+ project.owner.max_memberships_public_projects = None
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_private_project_can_be_public_because_owner_has_unlimited_slot(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = None
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_private_project_can_be_public_because_project_has_unlimited_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = None
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+# public to private
+
+def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot_and_too_much_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 0
+ project.owner.max_memberships_private_projects = 3
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 0
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_public_project_cant_be_private_because_too_much_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = 3
+
+ assert check_if_project_privacity_can_be_changed(project) == False
+
+
+def test_public_project_can_be_private_because_owner_has_enought_slot_and_project_has_enought_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_public_project_can_be_private_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = None
+ project.owner.max_memberships_private_projects = None
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_public_project_can_be_private_because_owner_has_unlimited_slot(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = None
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_privacity_can_be_changed(project) == True
+
+
+def test_public_project_can_be_private_because_project_has_unlimited_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = None
+
+ assert check_if_project_privacity_can_be_changed(project) == True
From 239c556ff53986d5fc400fe81139672fef1c068c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 17 Mar 2016 17:16:12 +0100
Subject: [PATCH 078/105] Move 'can_is_private_be_updated' to
'is_private_extra_info'
---
taiga/projects/serializers.py | 8 +++---
taiga/projects/services/members.py | 29 ++++++++++++++-------
tests/integration/test_projects.py | 42 +++++++++++++++++++-----------
3 files changed, 51 insertions(+), 28 deletions(-)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 28e3593c..3c649ece 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -76,7 +76,6 @@ class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
-
class Meta:
model = models.TaskStatus
i18n_fields = ("name",)
@@ -352,23 +351,24 @@ class ProjectDetailSerializer(ProjectSerializer):
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
+ is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
- can_is_private_be_updated = serializers.SerializerMethodField(method_name="get_can_is_private_be_updated")
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
+ def get_is_private_extra_info(self, obj):
+ return services.check_if_project_privacity_can_be_changed(obj)
+
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
def get_total_memberships(self, obj):
return services.get_total_project_memberships(obj)
- def get_can_is_private_be_updated(self, obj):
- return services.check_if_project_privacity_can_be_changed(obj)
######################################################
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index a141f8e7..3c9d0242 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -84,6 +84,11 @@ def get_total_project_memberships(project):
return project.memberships.count()
+ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships'
+ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships'
+ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
+ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
+
def check_if_project_privacity_can_be_changed(project):
"""Return if the project privacity can be changed from private to public or viceversa.
@@ -92,20 +97,26 @@ def check_if_project_privacity_can_be_changed(project):
:return: True if it can be changed or False if can't.
"""
if project.is_private:
+ current_memberships = project.memberships.count()
+ max_memberships = project.owner.max_memberships_public_projects
+ error_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
+
current_projects = project.owner.owned_projects.filter(is_private=False).count()
max_projects = project.owner.max_public_projects
- max_memberships = project.owner.max_memberships_public_projects
+ error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
else:
+ current_memberships = project.memberships.count()
+ max_memberships = project.owner.max_memberships_private_projects
+ error_members_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
+
current_projects = project.owner.owned_projects.filter(is_private=True).count()
max_projects = project.owner.max_private_projects
- max_memberships = project.owner.max_memberships_private_projects
-
- if max_projects is not None and current_projects >= max_projects:
- return False
-
- current_memberships = project.memberships.count()
+ error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
if max_memberships is not None and current_memberships > max_memberships:
- return False
+ return {'can_be_updated': False, 'reason': error_members_exceeded}
- return True
+ if max_projects is not None and current_projects >= max_projects:
+ return {'can_be_updated': False, 'reason': error_project_exceeded}
+
+ return {'can_be_updated': True, 'reason': None}
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 77592abc..0a0be800 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1399,7 +1399,13 @@ def test_project_transfer_validate_token_from_admin_member_with_valid_token(clie
# Test taiga.projects.services.members.check_if_project_privacity_can_be_changed
####################################################################################
-from taiga.projects.services import check_if_project_privacity_can_be_changed
+from taiga.projects.services.members import (
+ check_if_project_privacity_can_be_changed,
+ ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS,
+ ERROR_MAX_PUBLIC_PROJECTS,
+ ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS,
+ ERROR_MAX_PRIVATE_PROJECTS
+)
# private to public
@@ -1413,7 +1419,8 @@ def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot_a
project.owner.max_public_projects = 0
project.owner.max_memberships_public_projects = 3
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(client):
@@ -1426,7 +1433,8 @@ def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(c
project.owner.max_public_projects = 0
project.owner.max_memberships_public_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS})
def test_private_project_cant_be_public_because_too_much_members(client):
@@ -1439,7 +1447,8 @@ def test_private_project_cant_be_public_because_too_much_members(client):
project.owner.max_public_projects = 2
project.owner.max_memberships_public_projects = 3
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
def test_private_project_can_be_public_because_owner_has_enought_slot_and_project_has_enought_members(client):
@@ -1452,7 +1461,7 @@ def test_private_project_can_be_public_because_owner_has_enought_slot_and_projec
project.owner.max_public_projects = 2
project.owner.max_memberships_public_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_private_project_can_be_public_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
@@ -1465,7 +1474,7 @@ def test_private_project_can_be_public_because_owner_has_unlimited_slot_and_proj
project.owner.max_public_projects = None
project.owner.max_memberships_public_projects = None
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_private_project_can_be_public_because_owner_has_unlimited_slot(client):
@@ -1478,7 +1487,7 @@ def test_private_project_can_be_public_because_owner_has_unlimited_slot(client):
project.owner.max_public_projects = None
project.owner.max_memberships_public_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_private_project_can_be_public_because_project_has_unlimited_members(client):
@@ -1491,7 +1500,7 @@ def test_private_project_can_be_public_because_project_has_unlimited_members(cli
project.owner.max_public_projects = 2
project.owner.max_memberships_public_projects = None
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
# public to private
@@ -1506,7 +1515,8 @@ def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot_a
project.owner.max_private_projects = 0
project.owner.max_memberships_private_projects = 3
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(client):
@@ -1519,7 +1529,8 @@ def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(c
project.owner.max_private_projects = 0
project.owner.max_memberships_private_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS})
def test_public_project_cant_be_private_because_too_much_members(client):
@@ -1532,7 +1543,8 @@ def test_public_project_cant_be_private_because_too_much_members(client):
project.owner.max_private_projects = 2
project.owner.max_memberships_private_projects = 3
- assert check_if_project_privacity_can_be_changed(project) == False
+ assert (check_if_project_privacity_can_be_changed(project) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
def test_public_project_can_be_private_because_owner_has_enought_slot_and_project_has_enought_members(client):
@@ -1545,7 +1557,7 @@ def test_public_project_can_be_private_because_owner_has_enought_slot_and_projec
project.owner.max_private_projects = 2
project.owner.max_memberships_private_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_public_project_can_be_private_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
@@ -1558,7 +1570,7 @@ def test_public_project_can_be_private_because_owner_has_unlimited_slot_and_proj
project.owner.max_private_projects = None
project.owner.max_memberships_private_projects = None
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_public_project_can_be_private_because_owner_has_unlimited_slot(client):
@@ -1571,7 +1583,7 @@ def test_public_project_can_be_private_because_owner_has_unlimited_slot(client):
project.owner.max_private_projects = None
project.owner.max_memberships_private_projects = 6
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
def test_public_project_can_be_private_because_project_has_unlimited_members(client):
@@ -1584,4 +1596,4 @@ def test_public_project_can_be_private_because_project_has_unlimited_members(cli
project.owner.max_private_projects = 2
project.owner.max_memberships_private_projects = None
- assert check_if_project_privacity_can_be_changed(project) == True
+ assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
From 11b6cc061c8084d44f5f5139cecfc1b413f4fb8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 17 Mar 2016 19:46:34 +0100
Subject: [PATCH 079/105] Transfer project ownership to no admin members too
---
taiga/projects/api.py | 9 +-
taiga/projects/permissions.py | 6 +-
taiga/projects/services/transfer.py | 9 +-
tests/factories.py | 8 +-
tests/integration/test_projects.py | 343 +++++++++++++++-------------
5 files changed, 205 insertions(+), 170 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 43388afa..ee8aa50d 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -358,15 +358,12 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
user_model = apps.get_model("users", "User")
try:
user = user_model.objects.get(id=user_id)
-
except user_model.DoesNotExist:
return response.BadRequest(_("The user doesn't exist"))
- # Check the user is an admin membership from the project
- try:
- project.memberships.get(is_admin=True, user=user)
- except apps.get_model("projects", "Membership").DoesNotExist:
- return response.BadRequest(_("The user must be an admin member of the project"))
+ # Check the user is a membership from the project
+ if not project.memberships.filter(user=user).exists():
+ return response.BadRequest(_("The user must be a member of the project"))
reason = request.DATA.get('reason', None)
transfer_token = services.start_project_transfer(project, user, reason)
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index 0db55117..de65cd69 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -77,11 +77,11 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
- transfer_validate_token_perms = IsProjectAdmin()
+ transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_request_perms = IsProjectAdmin()
transfer_start_perms = IsMainOwner()
- transfer_reject_perms = IsProjectAdmin()
- transfer_accept_perms = IsProjectAdmin()
+ transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
class ProjectFansPermission(TaigaResourcePermission):
diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py
index fcd73ef0..17b0bbb1 100644
--- a/taiga/projects/services/transfer.py
+++ b/taiga/projects/services/transfer.py
@@ -90,12 +90,19 @@ def reject_project_transfer(project, user, token, reason):
def accept_project_transfer(project, user, token, reason):
validate_project_transfer_token(token, project, user)
- old_owner = project.owner
+ # Set new owner as project admin
+ membership = project.memberships.get(user=user)
+ if not membership.is_admin:
+ membership.is_admin = True
+ membership.save()
+ # Change the owner of the project
+ old_owner = project.owner
project.transfer_token = None
project.owner = user
project.save()
+ # Send mail
template = mail_builder.transfer_accept
context = {
"project": project,
diff --git a/tests/factories.py b/tests/factories.py
index d5d6d06b..71de1044 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -26,6 +26,9 @@ from .utils import DUMMY_BMP_DATA
import factory
+from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+
+
class Factory(factory.DjangoModelFactory):
class Meta:
@@ -555,8 +558,9 @@ def create_membership(**kwargs):
defaults = {
"project": project,
- "user": project.owner,
- "role": RoleFactory.create(project=project)
+ "user": UserFactory.create(),
+ "role": RoleFactory.create(project=project,
+ permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
}
defaults.update(kwargs)
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 0a0be800..f467302b 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -739,6 +739,11 @@ def test_project_list_with_search_query_order_by_ranking(client):
assert response.data[2]["id"] == project1.id
+####################################################################################
+# Test transfer project ownership
+####################################################################################
+
+
def test_transfer_request_from_not_anonimous(client):
user = f.UserFactory.create()
project = f.create_project(anon_permissions=["view_project"])
@@ -770,7 +775,7 @@ def test_transfer_request_from_not_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
- f.MembershipFactory(user=user, project=project, role=role, is_admin=False)
+ f.create_membership(user=user, project=project, role=role, is_admin=False)
url = reverse("projects-transfer-request", args=(project.id,))
@@ -786,7 +791,7 @@ def test_transfer_request_from_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
- f.MembershipFactory(user=user, project=project, role=role, is_admin=True)
+ f.create_membership(user=user, project=project, role=role, is_admin=True)
url = reverse("projects-transfer-request", args=(project.id,))
@@ -801,7 +806,7 @@ def test_transfer_request_from_admin_member(client):
def test_project_transfer_start_to_not_a_membership(client):
user_from = f.UserFactory.create()
project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
@@ -814,30 +819,12 @@ def test_project_transfer_start_to_not_a_membership(client):
assert "The user doesn't exist" in response.data
-def test_project_transfer_start_to_not_a_membership_admin(client):
+def test_project_transfer_start_to_a_not_admin_member(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project)
-
- client.login(user_from)
- url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
-
- data = {
- "user": user_to.id,
- }
- response = client.json.post(url, json.dumps(data))
- assert response.status_code == 400
- assert "The user must be" in response.data
-
-
-def test_project_transfer_start_to_a_valid_user(client):
- user_from = f.UserFactory.create()
- user_to = f.UserFactory.create()
- project = f.create_project(owner=user_from)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
@@ -855,7 +842,30 @@ def test_project_transfer_start_to_a_valid_user(client):
assert len(mail.outbox) == 1
-def test_project_transfer_reject_from_admin_member_without_token(client):
+def test_project_transfer_start_to_an_admin_member(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+ project = f.create_project(owner=user_from)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project, is_admin=True)
+
+ client.login(user_from)
+ url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
+
+ data = {
+ "user": user_to.id,
+ }
+ mail.outbox = []
+
+ assert project.transfer_token is None
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.id)
+ assert project.transfer_token is not None
+ assert len(mail.outbox) == 1
+
+
+def test_project_transfer_reject_from_member_without_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -863,8 +873,8 @@ def test_project_transfer_reject_from_admin_member_without_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -878,39 +888,14 @@ def test_project_transfer_reject_from_admin_member_without_token(client):
assert len(mail.outbox) == 0
-def test_project_transfer_reject_from_not_admin_member(client):
- user_from = f.UserFactory.create()
- user_to = f.UserFactory.create()
-
- signer = signing.TimestampSigner()
- token = signer.sign(user_to.id)
- project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
-
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=False)
-
- client.login(user_to)
- url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
-
- data = {
- "token": token,
- }
- mail.outbox = []
-
- response = client.json.post(url, json.dumps(data))
-
- assert response.status_code == 403
- assert len(mail.outbox) == 0
-
-
-def test_project_transfer_reject_from_admin_member_with_invalid_token(client):
+def test_project_transfer_reject_from_member_with_invalid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from, transfer_token="invalid-token")
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -927,7 +912,7 @@ def test_project_transfer_reject_from_admin_member_with_invalid_token(client):
assert len(mail.outbox) == 0
-def test_project_transfer_reject_from_admin_member_with_other_user_token(client):
+def test_project_transfer_reject_from_member_with_other_user_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
other_user = f.UserFactory.create()
@@ -936,8 +921,8 @@ def test_project_transfer_reject_from_admin_member_with_other_user_token(client)
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -954,7 +939,7 @@ def test_project_transfer_reject_from_admin_member_with_other_user_token(client)
assert len(mail.outbox) == 0
-def test_project_transfer_reject_from_admin_member_with_expired_token(client):
+def test_project_transfer_reject_from_member_with_expired_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -962,8 +947,8 @@ def test_project_transfer_reject_from_admin_member_with_expired_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -988,8 +973,8 @@ def test_project_transfer_reject_from_admin_member_with_valid_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
@@ -1004,9 +989,12 @@ def test_project_transfer_reject_from_admin_member_with_valid_token(client):
assert response.status_code == 200
assert len(mail.outbox) == 1
assert mail.outbox[0].to == [user_from.email]
+ project = Project.objects.get(pk=project.pk)
+ assert project.owner.id == user_from.id
+ assert project.transfer_token is None
-def test_project_transfer_accept_from_admin_member_without_token(client):
+def test_project_transfer_reject_from_no_admin_member_with_valid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -1014,8 +1002,40 @@ def test_project_transfer_accept_from_admin_member_without_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ m = f.create_membership(user=user_to, project=project, is_admin=False)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+ assert len(mail.outbox) == 1
+ assert mail.outbox[0].to == [user_from.email]
+ assert m.is_admin == False
+ project = Project.objects.get(pk=project.pk)
+ m = project.memberships.get(user=user_to)
+ assert project.owner.id == user_from.id
+ assert project.transfer_token is None
+ assert m.is_admin == False
+
+
+def test_project_transfer_accept_from_member_without_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create()
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token)
+
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1029,39 +1049,14 @@ def test_project_transfer_accept_from_admin_member_without_token(client):
assert len(mail.outbox) == 0
-def test_project_transfer_accept_from_not_admin_member(client):
- user_from = f.UserFactory.create()
- user_to = f.UserFactory.create()
-
- signer = signing.TimestampSigner()
- token = signer.sign(user_to.id)
- project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
-
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=False)
-
- client.login(user_to)
- url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
-
- data = {
- "token": token,
- }
- mail.outbox = []
-
- response = client.json.post(url, json.dumps(data))
-
- assert response.status_code == 403
- assert len(mail.outbox) == 0
-
-
-def test_project_transfer_accept_from_admin_member_with_invalid_token(client):
+def test_project_transfer_accept_from_member_with_invalid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from, transfer_token="invalid-token")
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1078,7 +1073,7 @@ def test_project_transfer_accept_from_admin_member_with_invalid_token(client):
assert len(mail.outbox) == 0
-def test_project_transfer_accept_from_admin_member_with_other_user_token(client):
+def test_project_transfer_accept_from_member_with_other_user_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
other_user = f.UserFactory.create()
@@ -1087,8 +1082,8 @@ def test_project_transfer_accept_from_admin_member_with_other_user_token(client)
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1105,7 +1100,7 @@ def test_project_transfer_accept_from_admin_member_with_other_user_token(client)
assert len(mail.outbox) == 0
-def test_project_transfer_accept_from_admin_member_with_expired_token(client):
+def test_project_transfer_accept_from_member_with_expired_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -1113,8 +1108,8 @@ def test_project_transfer_accept_from_admin_member_with_expired_token(client):
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1131,7 +1126,7 @@ def test_project_transfer_accept_from_admin_member_with_expired_token(client):
assert len(mail.outbox) == 0
-def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_slots(client):
+def test_project_transfer_accept_from_member_with_valid_token_without_enough_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_private_projects=0)
@@ -1139,8 +1134,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1159,7 +1154,7 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
assert project.transfer_token is not None
-def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_public_project_slots(client):
+def test_project_transfer_accept_from_member_with_valid_token_without_enough_memberships_public_project_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_memberships_public_projects=5)
@@ -1167,14 +1162,14 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=False)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1193,7 +1188,7 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
assert project.transfer_token is not None
-def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_private_project_slots(client):
+def test_project_transfer_accept_from_member_with_valid_token_without_enough_memberships_private_project_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_memberships_private_projects=5)
@@ -1201,14 +1196,14 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_without_enou
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
- f.MembershipFactory(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
+ f.create_membership(project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1235,8 +1230,8 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project, is_admin=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
@@ -1256,7 +1251,39 @@ def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_
assert project.transfer_token is None
-def test_project_transfer_validate_token_from_admin_member_without_token(client):
+def test_project_transfer_accept_from_no_admin_member_with_valid_token_with_enough_slots(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_private_projects=1)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ m = f.create_membership(user=user_to, project=project, is_admin=False)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+ mail.outbox = []
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+ assert len(mail.outbox) == 1
+ assert mail.outbox[0].to == [user_from.email]
+ assert m.is_admin == False
+ project = Project.objects.get(pk=project.pk)
+ m = project.memberships.get(user=user_to)
+ assert project.owner.id == user_to.id
+ assert project.transfer_token is None
+ assert m.is_admin == True
+
+
+def test_project_transfer_validate_token_from_member_without_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -1264,8 +1291,8 @@ def test_project_transfer_validate_token_from_admin_member_without_token(client)
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
@@ -1277,37 +1304,14 @@ def test_project_transfer_validate_token_from_admin_member_without_token(client)
assert response.status_code == 400
-def test_project_transfer_validate_token_from_not_admin_member(client):
- user_from = f.UserFactory.create()
- user_to = f.UserFactory.create()
-
- signer = signing.TimestampSigner()
- token = signer.sign(user_to.id)
- project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
-
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=False)
-
- client.login(user_to)
- url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
-
- data = {
- "token": token,
- }
-
- response = client.json.post(url, json.dumps(data))
-
- assert response.status_code == 403
-
-
-def test_project_transfer_validate_token_from_admin_member_with_invalid_token(client):
+def test_project_transfer_validate_token_from_member_with_invalid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from, transfer_token="invalid-token")
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
@@ -1322,7 +1326,7 @@ def test_project_transfer_validate_token_from_admin_member_with_invalid_token(cl
assert "Token is invalid" == response.data["_error_message"]
-def test_project_transfer_validate_token_from_admin_member_with_other_user_token(client):
+def test_project_transfer_validate_token_from_member_with_other_user_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
other_user = f.UserFactory.create()
@@ -1331,8 +1335,8 @@ def test_project_transfer_validate_token_from_admin_member_with_other_user_token
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
@@ -1347,7 +1351,7 @@ def test_project_transfer_validate_token_from_admin_member_with_other_user_token
assert "Token is invalid" == response.data["_error_message"]
-def test_project_transfer_validate_token_from_admin_member_with_expired_token(client):
+def test_project_transfer_validate_token_from_member_with_expired_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
@@ -1355,8 +1359,8 @@ def test_project_transfer_validate_token_from_admin_member_with_expired_token(cl
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project)
client.login(user_to)
url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
@@ -1380,8 +1384,31 @@ def test_project_transfer_validate_token_from_admin_member_with_valid_token(clie
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
- f.MembershipFactory(user=user_from, project=project, is_admin=True)
- f.MembershipFactory(user=user_to, project=project, is_admin=True)
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project, is_admin=True)
+
+ client.login(user_to)
+ url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
+
+ data = {
+ "token": token,
+ }
+
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 200
+
+
+def test_project_transfer_validate_token_from_no_admin_member_with_valid_token(client):
+ user_from = f.UserFactory.create()
+ user_to = f.UserFactory.create(max_private_projects=1)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
+
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, project=project, is_admin=False)
client.login(user_to)
url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk})
From af588be5276fca1cc5d9b593152b643e53a8930e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 17 Mar 2016 20:42:10 +0100
Subject: [PATCH 080/105] Project owners can not be no admin
---
taiga/projects/serializers.py | 10 +++++++---
tests/integration/test_memberships.py | 14 ++++++++++++++
2 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 3c649ece..93de28c3 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -190,9 +190,13 @@ class MembershipSerializer(serializers.ModelSerializer):
if project is None:
project = self.object.project
- if (self.object and
- not services.project_has_valid_admins(project, exclude_user=self.object.user)):
- raise serializers.ValidationError(_("In this project at least one of the users must be an active admin."))
+ if (self.object):
+ if self.object.user.id == project.owner_id and attrs[source] != True:
+ raise serializers.ValidationError(_("Project owner must be admin."))
+
+ if not services.project_has_valid_admins(project, exclude_user=self.object.user):
+ raise serializers.ValidationError(_("In this project at least one of the users "
+ "must be an active admin."))
return attrs
diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py
index a408f838..3b9df18c 100644
--- a/tests/integration/test_memberships.py
+++ b/tests/integration/test_memberships.py
@@ -347,6 +347,20 @@ def test_api_edit_membership(client):
assert response.status_code == 200
+def test_api_change_owner_membership_to_no_admin_return_error(client):
+ project = f.ProjectFactory()
+ membership_owner = f.MembershipFactory(project=project, user=project.owner, is_admin=True)
+ membership = f.MembershipFactory(project=project, is_admin=True)
+
+ url = reverse("memberships-detail", args=[membership_owner.id])
+ data = {"is_admin": False}
+
+ client.login(membership.user)
+ response = client.json.patch(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert 'is_admin' in response.data
+
def test_api_delete_membership(client):
membership = f.MembershipFactory(is_admin=True)
From f675cea441ef21898e7c7be8fae59eb502f91fc3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 18 Mar 2016 11:00:29 +0100
Subject: [PATCH 081/105] Fix: reverse error messages
---
taiga/projects/services/members.py | 8 ++++----
tests/integration/test_projects.py | 12 ++++++------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index 3c9d0242..f408d430 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -99,19 +99,19 @@ def check_if_project_privacity_can_be_changed(project):
if project.is_private:
current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_public_projects
- error_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
+ error_members_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
current_projects = project.owner.owned_projects.filter(is_private=False).count()
max_projects = project.owner.max_public_projects
- error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
+ error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
else:
current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_private_projects
- error_members_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
+ error_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
current_projects = project.owner.owned_projects.filter(is_private=True).count()
max_projects = project.owner.max_private_projects
- error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
+ error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
if max_memberships is not None and current_memberships > max_memberships:
return {'can_be_updated': False, 'reason': error_members_exceeded}
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index f467302b..1a6ca2a4 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1447,7 +1447,7 @@ def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot_a
project.owner.max_memberships_public_projects = 3
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(client):
@@ -1461,7 +1461,7 @@ def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(c
project.owner.max_memberships_public_projects = 6
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS})
def test_private_project_cant_be_public_because_too_much_members(client):
@@ -1475,7 +1475,7 @@ def test_private_project_cant_be_public_because_too_much_members(client):
project.owner.max_memberships_public_projects = 3
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
def test_private_project_can_be_public_because_owner_has_enought_slot_and_project_has_enought_members(client):
@@ -1543,7 +1543,7 @@ def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot_a
project.owner.max_memberships_private_projects = 3
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(client):
@@ -1557,7 +1557,7 @@ def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(c
project.owner.max_memberships_private_projects = 6
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS})
def test_public_project_cant_be_private_because_too_much_members(client):
@@ -1571,7 +1571,7 @@ def test_public_project_cant_be_private_because_too_much_members(client):
project.owner.max_memberships_private_projects = 3
assert (check_if_project_privacity_can_be_changed(project) ==
- {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
def test_public_project_can_be_private_because_owner_has_enought_slot_and_project_has_enought_members(client):
From 5468d0ed346d034ea5e25b7b71f1051490d48196 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 18 Mar 2016 19:04:19 +0100
Subject: [PATCH 082/105] Fix is_project_owner
---
taiga/permissions/service.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py
index adf8cb38..a56b3afc 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/service.py
@@ -38,9 +38,6 @@ def _get_object_project(obj):
def is_project_owner(user, obj):
- if user.is_superuser:
- return True
-
project = _get_object_project(obj)
if project is None:
return False
From c29f1a5324d81f3e124ed09ca8cb9195723189aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 22 Mar 2016 10:31:21 +0100
Subject: [PATCH 083/105] Fix is_admin validation for memberships without user
---
taiga/projects/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 93de28c3..11720532 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -190,7 +190,7 @@ class MembershipSerializer(serializers.ModelSerializer):
if project is None:
project = self.object.project
- if (self.object):
+ if (self.object and self.object.user):
if self.object.user.id == project.owner_id and attrs[source] != True:
raise serializers.ValidationError(_("Project owner must be admin."))
From 5b61586925ce517bc31a02e58699c967f4d4d3be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 22 Mar 2016 13:00:55 +0100
Subject: [PATCH 084/105] Add missed migration
---
.../migrations/0039_auto_20160322_1157.py | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 taiga/projects/migrations/0039_auto_20160322_1157.py
diff --git a/taiga/projects/migrations/0039_auto_20160322_1157.py b/taiga/projects/migrations/0039_auto_20160322_1157.py
new file mode 100644
index 00000000..1aa98e00
--- /dev/null
+++ b/taiga/projects/migrations/0039_auto_20160322_1157.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-03-22 11:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0038_auto_20160215_1133'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='blocked_code',
+ field=models.CharField(blank=True, choices=[('blocked-by-nonpayment', 'This project was blocked by nonpayment'), ('blocked-by-staff', 'This project was blocked by staff'), ('blocked-by-owner-leaving', 'This project was blocked because the owner left')], default=None, max_length=255, null=True, verbose_name='blocked code'),
+ ),
+ ]
From 995c753a4b44b617906983b418f6032ddee84a9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 23 Mar 2016 13:47:07 +0100
Subject: [PATCH 085/105] Issue #225 (taiga-payments): Remove blocked project
from discover calls
---
taiga/projects/filters.py | 3 ++-
taiga/projects/models.py | 8 ++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index b4106a3c..f01ed179 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -37,7 +37,8 @@ class DiscoverModeFilterBackend(FilterBackend):
if discover_mode:
# discover_mode enabled
- qs = qs.filter(anon_permissions__contains=["view_project"])
+ qs = qs.filter(anon_permissions__contains=["view_project"],
+ blocked_code__isnull=True)
return super().filter_queryset(request, qs.distinct(), view)
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 5cbf5554..59d16f9a 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -220,6 +220,10 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
verbose_name=_("project transfer token"))
+ blocked_code = models.CharField(null=True, blank=True, max_length=255,
+ choices=choices.BLOCKING_CODES + settings.EXTRA_BLOCKING_CODES, default=None,
+ verbose_name=_("blocked code"))
+
#Totals:
totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("updated date time"), db_index=True)
@@ -248,10 +252,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
total_activity_last_year = models.PositiveIntegerField(null=False, blank=False, default=0,
verbose_name=_("activity last year"), db_index=True)
- blocked_code = models.CharField(null=True, blank=True, max_length=255,
- choices=choices.BLOCKING_CODES + settings.EXTRA_BLOCKING_CODES, default=None,
- verbose_name=_("blocked code"))
-
_importing = None
class Meta:
From f8cb6d9033a15cf4b944c110c7034092c450e60b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 25 Mar 2016 12:21:32 +0100
Subject: [PATCH 086/105] [i18n] Update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/de/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/en/LC_MESSAGES/django.po | 92 ++++++-----
taiga/locale/es/LC_MESSAGES/django.po | 175 ++++++++++++++-------
taiga/locale/fi/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/fr/LC_MESSAGES/django.po | 116 ++++++++------
taiga/locale/it/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/nl/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/pl/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/pt_BR/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/ru/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/sv/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/tr/LC_MESSAGES/django.po | 94 ++++++-----
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 94 ++++++-----
14 files changed, 793 insertions(+), 624 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 4b4afcbb..934e4f4a 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -66,8 +66,8 @@ msgid "Error on creating new user."
msgstr "Error creant un nou usuari."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Token invàlid"
@@ -184,7 +184,7 @@ msgstr ""
"està corrupte."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr ""
@@ -1147,41 +1147,41 @@ msgstr "Administrar valors de projecte"
msgid "Admin roles"
msgstr "Administrar rols"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Arguments incomplets."
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Format d'image invàlid"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
@@ -1270,11 +1270,15 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1696,41 +1700,41 @@ msgstr "colors de tags"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Actualitzada data"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2337,51 +2341,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Aquest e-mail ja està en ús"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rol invàlid per al projecte"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rols"
@@ -2389,7 +2397,7 @@ msgstr "Rols"
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 96010dfd..64a4be90 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -76,8 +76,8 @@ msgid "Error on creating new user."
msgstr "Fehler bei der Erstellung des neuen Benutzers."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Ungültiges Token"
@@ -214,7 +214,7 @@ msgstr ""
"haben, ist entweder kein Bild oder defekt."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
@@ -1337,41 +1337,41 @@ msgstr "Administrator Projekt Werte"
msgid "Admin roles"
msgstr "Administrator-Rollen"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Unvollständige Argumente"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ungültiges Bildformat"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Unglültiger Templatename"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Ungültige Templatebeschreibung"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
@@ -1460,11 +1460,15 @@ msgstr "Kunde"
msgid "Talky"
msgstr "Gesprächig"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1890,41 +1894,41 @@ msgstr "Tag Farben"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "Aktualisierungsdatum"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "Count"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2809,51 +2813,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Die E-Mailadresse ist bereits vergeben"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ungültige Rolle für dieses Projekt"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rollen"
@@ -2861,7 +2869,7 @@ msgstr "Rollen"
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Projektende"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index c97be62f..7d3e8117 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-03-08 18:27+0100\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -62,8 +62,8 @@ msgid "Error on creating new user."
msgstr ""
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr ""
@@ -176,7 +176,7 @@ msgid ""
msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr ""
@@ -1120,41 +1120,41 @@ msgstr ""
msgid "Admin roles"
msgstr ""
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr ""
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr ""
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr ""
@@ -1243,11 +1243,15 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1669,41 +1673,41 @@ msgstr ""
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr ""
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2304,51 +2308,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr ""
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr ""
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr ""
@@ -2356,7 +2364,7 @@ msgstr ""
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index f40dcf27..a0c91622 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -8,13 +8,15 @@
# gustavodiazjaimes , 2015
# Hector Colina , 2015
# Jesus Marin , 2015
+# Luis Sebastian Urrutia Fuentes , 2016
+# Renelis Abreu Ramirez , 2016
# Taiga Dev Team , 2015-2016
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -70,8 +72,8 @@ msgid "Error on creating new user."
msgstr "Error al crear un nuevo usuario "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Token inválido"
@@ -198,13 +200,13 @@ msgid ""
msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
#: taiga/webhooks/api.py:67
msgid "Blocked element"
-msgstr ""
+msgstr "Elemento bloqueado"
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
@@ -359,14 +361,14 @@ msgstr "Error por incumplimiento de precondición"
#: taiga/base/exceptions.py:217
msgid "Not enough slots for project."
-msgstr ""
+msgstr "No hay suficiente espacio para el proyecto."
#: taiga/base/filters.py:79 taiga/base/filters.py:444
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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
@@ -1314,41 +1316,43 @@ msgstr "Administrar valores de proyecto"
msgid "Admin roles"
msgstr "Administrar roles"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagen no válido"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nombre de plantilla invalido"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Descripción de plantilla invalida"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
-msgstr ""
+msgstr "id de usuario inválido"
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
+msgstr "El usuario no existe"
+
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
-msgstr ""
-
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
+"El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un "
+"administrador activo"
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
@@ -1437,13 +1441,17 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
-msgid "This project was blocked by staff"
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:33
+msgid "This project was blocked by staff"
+msgstr "El proyecto fue bloqueado por la administración"
+
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
-msgstr ""
+msgstr "El proyecto fue bloqueado porque el dueño lo abandonó"
#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
@@ -1459,7 +1467,7 @@ msgstr "Fecha"
#: taiga/projects/custom_attributes/choices.py:30
msgid "Url"
-msgstr ""
+msgstr "Url"
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
@@ -1863,45 +1871,45 @@ msgstr "colores de etiquetas"
#: taiga/projects/models.py:221
msgid "project transfer token"
-msgstr ""
+msgstr "token de transferencia de proyecto"
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr "código bloqueado"
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "fecha y hora de actualización"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "recuento"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans la última semana"
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans el último mes"
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans el último año"
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "actividad la última semana"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "actividad el último mes"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configuración de modulos"
@@ -2736,52 +2744,60 @@ msgstr "versión"
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
+"No puedes abandonar el proyecto si eres el dueño o no existen más "
+"administradores"
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "La dirección de email ya está en uso."
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rol inválido para el proyecto"
-#: taiga/projects/serializers.py:196
-msgid "In this project at least one of the users must be an active admin."
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:198
+msgid "In this project at least one of the users must be an active admin."
+msgstr ""
+"En este proyecto al menos uno de los usuarios debe ser un administrador "
+"activo."
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roles"
@@ -2789,7 +2805,7 @@ msgstr "Roles"
msgid "Future sprint"
msgstr "Sprint futuro"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Final de proyecto"
@@ -2802,7 +2818,7 @@ msgstr "token inválido"
#: taiga/projects/services/transfer.py:66
msgid "Token has expired"
-msgstr ""
+msgstr "El token ha expirado"
#: 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."
@@ -2987,10 +3003,15 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+" Hola %(old_owner_name)s,
\n"
+" %(new_owner_name)s ha aceptado tu proposición y será el nuevo dueño "
+"de \"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
msgid "This is the reason/comment:
"
-msgstr ""
+msgstr "Esta es la razón/comentario:
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -2998,6 +3019,10 @@ msgid ""
" From now on, your new status for this project will be \"admin\".
\n"
" "
msgstr ""
+"\n"
+" De ahora en adelante, tu rol para este proyecto será de \"admin\"."
+"p>\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -3007,18 +3032,24 @@ msgid ""
"%(new_owner_name)s has accepted your offer and will become the new project "
"owner for \"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Hola %(old_owner_name)s,\n"
+"%(new_owner_name)s ha aceptado tu proposición y será el nuevo dueño de "
+"\"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
msgid "This is the reason/comment:"
-msgstr ""
+msgstr "Esta es la razón/comentario:"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
+"\n"
+"De ahora en adelante, tu rol para este proyecto será de \"admin\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
@@ -3028,6 +3059,8 @@ msgid ""
"\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"El Equipo de Taiga\n"
#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
#, python-format
@@ -3035,6 +3068,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer offer accepted!\n"
msgstr ""
+"\n"
+"[%(project)s] ¡Proposición de transferencia de dueño aceptada!\n"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
#, python-format
@@ -3045,6 +3080,11 @@ msgid ""
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
+"\n"
+"Hola %(owner_name)s,
\n"
+"\n"
+"%(rejecter_name)s ha declinado tu oferta y no será el nuevo dueño del "
+"proyecto \"%(project_name)s\".
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
msgid ""
@@ -3052,6 +3092,8 @@ msgid ""
" This is the reason/comment:
\n"
" "
msgstr ""
+"\n"
+"Esta es la razón/comentario:
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
@@ -3060,11 +3102,14 @@ msgid ""
"a different person.\n"
" "
msgstr ""
+"\n"
+"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a "
+"una persona diferente.
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
msgid "Request transfer to a different person"
-msgstr ""
+msgstr "Solicitar transferir a una persona diferente"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
#, python-format
@@ -3074,6 +3119,10 @@ msgid ""
"%(rejecter_name)s has declined your offer and will not become the new "
"project owner for \"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Hola %(owner_name)s,\n"
+"%(rejecter_name)s ha declinado tu oferta y no será el dueño del nuevo "
+"proyecto \"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
msgid ""
@@ -3081,10 +3130,13 @@ msgid ""
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
+"\n"
+"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a "
+"una persona diferente.\n"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
msgid "Request transfer to a different person:"
-msgstr ""
+msgstr "Solicitar transferir a una persona diferente:"
#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
#, python-format
@@ -3092,6 +3144,9 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer declined\n"
msgstr ""
+"\n"
+"[%(project)s] Propiedad de transferencia de proyecto rechazada\n"
+"\n"
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
#, python-format
@@ -3186,7 +3241,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
msgid "Accept or reject the project transfer:"
-msgstr ""
+msgstr "Aceptar o rechazar la transferencia de proyecto:"
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
#, python-format
@@ -3540,7 +3595,7 @@ msgstr "Permisos"
#: taiga/users/admin.py:55
msgid "Restrictions"
-msgstr ""
+msgstr "Restricciones"
#: taiga/users/admin.py:57
msgid "Important dates"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 4f15c826..b72c130e 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -67,8 +67,8 @@ msgid "Error on creating new user."
msgstr "Virhe käyttäjän luonnissa."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Väärä tunniste"
@@ -186,7 +186,7 @@ msgstr ""
"vioittunut."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
@@ -1276,41 +1276,41 @@ msgstr "Hallinnoi projektin arvoja"
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Puutteelliset argumentit"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Väärä kuvaformaatti"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Virheellinen mallipohjan nimi"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Virheellinen mallipohjan kuvaus"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
@@ -1399,11 +1399,15 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1825,41 +1829,41 @@ msgstr "avainsanojen värit"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "päivityspvm"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2706,51 +2710,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Sähköpostiosoite on jo käytössä"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Virheellinen rooli projektille"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roolit"
@@ -2758,7 +2766,7 @@ msgstr "Roolit"
msgid "Future sprint"
msgstr "Tuleva kierros"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Projektin loppu"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index 065255a7..299ca2a5 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -15,14 +15,15 @@
# Nicolas Minelle , 2016
# Nlko , 2015
# Regis TEDONE , 2015
+# Sébastien Talbot , 2016
# Stéphane Mor , 2015
# William Godin , 2015
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -79,8 +80,8 @@ msgid "Error on creating new user."
msgstr "Erreur à la création du nouvel utilisateur."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Jeton invalide"
@@ -209,13 +210,13 @@ msgstr ""
"image ou était une image corrompue."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
#: taiga/webhooks/api.py:67
msgid "Blocked element"
-msgstr ""
+msgstr "Élément bloqué"
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
@@ -371,14 +372,14 @@ msgstr "Erreur de précondition"
#: taiga/base/exceptions.py:217
msgid "Not enough slots for project."
-msgstr ""
+msgstr "Pas d'emplacement libre pour ce projet."
#: 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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
@@ -814,6 +815,13 @@ msgid ""
" The Taiga Team
\n"
" "
msgstr ""
+"\n"
+"Importation du Dump projet
\n"
+"Bonjour %(user)s,
\n"
+"Le dump de votre projet a été correctement importé.
\n"
+"Allez au %(project)s\n"
+"L'équipe Taiga
"
#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1
#, python-format
@@ -1281,41 +1289,41 @@ msgstr "Administrer les paramètres du projet"
msgid "Admin roles"
msgstr "Administrer les rôles"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "arguments manquants"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "format de l'image non valide"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nom de modèle non valide"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Description du modèle non valide"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
-msgstr ""
+msgstr "Identifiant utilisateur invalide"
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
+msgstr "L'utilisateur n'existe pas"
+
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
-msgstr ""
-
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
@@ -1404,11 +1412,15 @@ msgstr "Personnalisé"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1426,7 +1438,7 @@ msgstr "Date"
#: taiga/projects/custom_attributes/choices.py:30
msgid "Url"
-msgstr ""
+msgstr "Url"
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
@@ -1830,43 +1842,43 @@ msgstr "couleurs des tags"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr "code bloqué"
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "date de mise à jour"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "total"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans la semaine dernière"
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans le mois dernier"
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans l'année dernière"
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "activité de la semaine écoulée"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "activité du mois écoulé"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "Configurations des modules"
@@ -2331,6 +2343,8 @@ msgid ""
"\n"
"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n"
msgstr ""
+"\n"
+"[%(project)s] Récit Utilisateur #%(ref)s \"%(subject)s\" créé\n"
#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4
#, python-format
@@ -2493,51 +2507,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Adresse email déjà existante"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rôle non valide pour le projet"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rôles"
@@ -2545,7 +2563,7 @@ msgstr "Rôles"
msgid "Future sprint"
msgstr "Sprint futurs"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Fin du projet"
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index e8f67327..0d55aa08 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,8 +14,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -73,8 +73,8 @@ msgid "Error on creating new user."
msgstr "Errore nella creazione dell'utente."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Token non valido"
@@ -195,7 +195,7 @@ msgstr ""
"o l'immagine potrebbe essere corrotta. "
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -363,7 +363,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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
@@ -1410,41 +1410,41 @@ msgstr "Valori dell'amministratore del progetto"
msgid "Admin roles"
msgstr "Ruoli dell'amministratore"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argomento non valido"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato dell'immagine non valido"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Il nome del template non è valido"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "La descrizione del template non è valida"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
@@ -1533,11 +1533,15 @@ msgstr "Personalizzato"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1960,43 +1964,43 @@ msgstr "colori dei tag"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "tempo e data aggiornati"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "conta"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans nella settimana"
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans nel mese"
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans nell'anno"
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "attività nella settimana"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "attività nel mese"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "configurazione dei moduli"
@@ -2981,51 +2985,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "L'indirizzo email è già usato"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ruolo di progetto non valido"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Ruoli"
@@ -3033,7 +3041,7 @@ msgstr "Ruoli"
msgid "Future sprint"
msgstr "Sprint futuri"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Termine di progetto"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index 3312f7c7..675f8761 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -66,8 +66,8 @@ msgid "Error on creating new user."
msgstr "Fout bij het aanmaken van een nieuwe gebruiker."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Ongeldig token"
@@ -195,7 +195,7 @@ msgstr ""
"een afbeelding ofwel een corrupte afbeelding."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
@@ -1210,41 +1210,41 @@ msgstr "Admin project waarden"
msgid "Admin roles"
msgstr "Admin rollen"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Onvolledige argumenten"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ongeldig afbeelding formaat"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Ongeldige template naam"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Ongeldige template omschrijving"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
@@ -1333,11 +1333,15 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1761,41 +1765,41 @@ msgstr "tag kleuren"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "gewijzigde datum en tijd"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2426,51 +2430,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "E-mail adres is al in gebruik"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ongeldige rol voor project"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rollen"
@@ -2478,7 +2486,7 @@ msgstr "Rollen"
msgid "Future sprint"
msgstr "Toekomstige sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Project einde"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 33c704bf..842ab5d4 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "Błąd przy tworzeniu użytkownika."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Nieprawidłowy token"
@@ -189,7 +189,7 @@ msgstr ""
"jest uszkodzony."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
@@ -1318,41 +1318,41 @@ msgstr "Administruj wartościami projektu"
msgid "Admin roles"
msgstr "Administruj rolami"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Pola niekompletne"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Niepoprawny format obrazka"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nieprawidłowa nazwa szablonu"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Nieprawidłowy opis szablonu"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
@@ -1441,11 +1441,15 @@ msgstr "Niestandardowy"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1867,41 +1871,41 @@ msgstr "kolory tagów"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data aktualizacji"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "ilość"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2763,51 +2767,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Tena adres e-mail jest już w użyciu"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Nieprawidłowa rola w projekcie"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Role"
@@ -2815,7 +2823,7 @@ msgstr "Role"
msgid "Future sprint"
msgstr "Przyszły sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Zakończenie projektu"
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 56f511c7..d2f7b9af 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+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"
@@ -76,8 +76,8 @@ msgid "Error on creating new user."
msgstr "Erro ao criar um novo usuário."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Token inválido"
@@ -196,7 +196,7 @@ msgstr ""
"está corrompido."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
@@ -1324,41 +1324,41 @@ msgstr "Valores projeto admin"
msgid "Admin roles"
msgstr "Funções Admin"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagem inválida"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nome de template inválido"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Descrição de template inválida"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
@@ -1447,11 +1447,15 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1873,41 +1877,41 @@ msgstr "cores de tags"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "data de atualização"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "contagem"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2747,51 +2751,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Endereço de e-mail já utilizado"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Função inválida para projeto"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Funções"
@@ -2799,7 +2807,7 @@ msgstr "Funções"
msgid "Future sprint"
msgstr "Sprint futuro"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Fim do projeto"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index e23090bc..11e1e1e3 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -74,8 +74,8 @@ msgid "Error on creating new user."
msgstr "Ошибка при создании нового пользователя."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Неверный токен"
@@ -199,7 +199,7 @@ msgstr ""
"изображение, либо не корректное изображение."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
@@ -1325,41 +1325,41 @@ msgstr "Управлять значениями проекта"
msgid "Admin roles"
msgstr "Управлять ролями"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Неправильный формат изображения"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Неверное название шаблона"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Неверное описание шаблона"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
@@ -1448,11 +1448,15 @@ msgstr "Специальный"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1880,43 +1884,43 @@ msgstr "цвета тэгов"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "дата и время обновления"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "количество"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "активность за неделю"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "активность за месяц"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "конфигурация модулей"
@@ -2762,51 +2766,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "Этот почтовый адрес уже используется"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Неверная роль для этого проекта"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Роли"
@@ -2814,7 +2822,7 @@ msgstr "Роли"
msgid "Future sprint"
msgstr "Будущий спринт"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Окончание проекта"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 4da6b6db..f314b9b2 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -65,8 +65,8 @@ msgid "Error on creating new user."
msgstr "Ett fel uppstod når användaren skapades. "
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Felaktig förekomst. "
@@ -188,7 +188,7 @@ msgstr ""
"eller en skadad bild."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
@@ -1165,41 +1165,41 @@ msgstr "Administrera projektvärden"
msgid "Admin roles"
msgstr "Administratorroller"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Felaktiga argument"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Felaktigt bildformat"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Inget giltigt mallnamn"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Inte giltigt mallbeskrivning"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
@@ -1288,11 +1288,15 @@ msgstr "Anpassa"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1714,41 +1718,41 @@ msgstr "färger för taggar"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "uppdaterad dato och tid"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "räkna"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:249
-msgid "activity last year"
-msgstr ""
-
#: taiga/projects/models.py:253
-msgid "blocked code"
+msgid "activity last year"
msgstr ""
#: taiga/projects/models.py:461
@@ -2349,51 +2353,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "E-postadressen är redan använd"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Fel roll for projektet"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roller"
@@ -2401,7 +2409,7 @@ msgstr "Roller"
msgid "Future sprint"
msgstr "Framtidig sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Projektslut"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 5e9d463b..3bd5749e 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "Geçersiz kupon"
@@ -196,7 +196,7 @@ msgstr ""
"resim dosyası değil."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
@@ -1272,41 +1272,41 @@ msgstr "Admin proje değerleri"
msgid "Admin roles"
msgstr "Yönetici rolleri"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Eksik parametreq"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Geçersiz resim biçemi"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Geçersiz şablon adı"
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "Geçersiz şablon tanımı"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
@@ -1395,11 +1395,15 @@ msgstr "Özel"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1821,43 +1825,43 @@ msgstr "etiket renkleri"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "yükleme tarih-saati"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "sayı"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "geçen hafta fanları"
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "geçen ayın fanları"
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "geçen yılın fanları"
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "geçen haftanın aktiviteleri"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "geçen ayın aktiviteleri"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "modül ayarları"
@@ -2522,51 +2526,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "E-posta adresi önceden alınmış"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Proje için geçersiz rol"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roller"
@@ -2574,7 +2582,7 @@ msgstr "Roller"
msgid "Future sprint"
msgstr "Gelecek sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "Proje Sonu"
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index 590b9978..cc2595bc 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-03-08 18:27+0100\n"
-"PO-Revision-Date: 2016-03-08 17:28+0000\n"
+"POT-Creation-Date: 2016-03-25 12:01+0100\n"
+"PO-Revision-Date: 2016-03-25 11:01+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"
@@ -68,8 +68,8 @@ msgid "Error on creating new user."
msgstr "無法創建新使用者"
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:370
-#: taiga/projects/api.py:392
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:398
msgid "Invalid token"
msgstr "無效的代碼 "
@@ -182,7 +182,7 @@ msgid ""
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:623
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
#: taiga/projects/issues/api.py:237 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
@@ -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:58
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
@@ -1298,41 +1298,41 @@ msgstr "管理員專案數值"
msgid "Admin roles"
msgstr "管理員角色"
-#: taiga/projects/api.py:164 taiga/users/api.py:220
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "不完整參數"
-#: taiga/projects/api.py:168 taiga/users/api.py:225
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "無效的圖片檔案"
-#: taiga/projects/api.py:229
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "非有效樣板名稱 "
-#: taiga/projects/api.py:232
+#: taiga/projects/api.py:233
msgid "Not valid template description"
msgstr "無效樣板描述"
-#: taiga/projects/api.py:347
+#: taiga/projects/api.py:356
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:354
+#: taiga/projects/api.py:362
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:360
-msgid "The user must be an admin member of the project"
+#: taiga/projects/api.py:366
+msgid "The user must be a member of the project"
msgstr ""
-#: taiga/projects/api.py:662
+#: taiga/projects/api.py:668
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:702
+#: taiga/projects/api.py:708
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
@@ -1421,11 +1421,15 @@ msgstr "自定"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:31
+#: taiga/projects/choices.py:32
+msgid "This project was blocked by nonpayment"
+msgstr ""
+
+#: taiga/projects/choices.py:33
msgid "This project was blocked by staff"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:34
msgid "This project was blocked because the owner left"
msgstr ""
@@ -1847,43 +1851,43 @@ msgstr "標籤顏色"
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:225
+msgid "blocked code"
+msgstr ""
+
+#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
msgstr "更新日期時間"
-#: taiga/projects/models.py:228 taiga/projects/models.py:240
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "數量"
-#: taiga/projects/models.py:231
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "上週粉絲"
-#: taiga/projects/models.py:234
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "上個月粉絲"
-#: taiga/projects/models.py:237
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "去年粉絲"
-#: taiga/projects/models.py:243
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "上週活躍成員"
-#: taiga/projects/models.py:246
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "上月活躍成員"
-#: taiga/projects/models.py:249
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:253
-msgid "blocked code"
-msgstr ""
-
#: taiga/projects/models.py:461
msgid "modules config"
msgstr "模組設定"
@@ -2732,51 +2736,55 @@ msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:173
+#: taiga/projects/serializers.py:172
msgid "Email address is already taken"
msgstr "電子郵件已使用"
-#: taiga/projects/serializers.py:185
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "專案無效的角色"
-#: taiga/projects/serializers.py:196
+#: taiga/projects/serializers.py:195
+msgid "Project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
msgstr ""
-#: taiga/projects/serializers.py:377
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:378
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:379
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:380
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:381
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:382
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:383
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:384
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:385
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "角色"
@@ -2784,7 +2792,7 @@ msgstr "角色"
msgid "Future sprint"
msgstr "未來之衝刺"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:218
msgid "Project End"
msgstr "專案結束"
From 0183e566384cdc4e93aae42f1ac97187a2aa7f30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Sun, 27 Mar 2016 13:05:56 +0200
Subject: [PATCH 087/105] [i18n] Update locales
---
taiga/locale/es/LC_MESSAGES/django.po | 28 +++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index a0c91622..afcb7414 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@
# This file is distributed under the same license as the taiga-back package.
#
# Translators:
-# David Barragán , 2015
+# David Barragán , 2015-2016
# Esther Moreno , 2015
# gustavodiazjaimes , 2015
# Hector Colina , 2015
@@ -16,8 +16,8 @@ msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"PO-Revision-Date: 2016-03-27 11:05+0000\n"
+"Last-Translator: David Barragán \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
"MIME-Version: 1.0\n"
@@ -1342,7 +1342,7 @@ msgstr "El usuario no existe"
#: taiga/projects/api.py:366
msgid "The user must be a member of the project"
-msgstr ""
+msgstr "El usuario debe ser miembro del proyecto"
#: taiga/projects/api.py:668
msgid ""
@@ -2757,7 +2757,7 @@ msgstr "Rol inválido para el proyecto"
#: taiga/projects/serializers.py:195
msgid "Project owner must be admin."
-msgstr ""
+msgstr "El propietario del proyecto debe ser administrador."
#: taiga/projects/serializers.py:198
msgid "In this project at least one of the users must be an active admin."
@@ -3169,7 +3169,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
msgid "Continue"
-msgstr ""
+msgstr "Continuar"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
#, python-format
@@ -3718,19 +3718,19 @@ msgstr "nueva dirección de email"
#: taiga/users/models.py:129
msgid "max number of private projects owned"
-msgstr ""
+msgstr "máximo de proyectos privados poseídos"
#: taiga/users/models.py:132
msgid "max number of public projects owned"
-msgstr ""
+msgstr "máximo de proyectos públicos poseídos"
#: taiga/users/models.py:135
msgid "max number of memberships for each owned private project"
-msgstr ""
+msgstr "máximo de membresías para cada proyecto privado poseído"
#: taiga/users/models.py:139
msgid "max number of memberships for each owned public project"
-msgstr ""
+msgstr "máximo de membresías para cada proyecto público poseído"
#: taiga/users/models.py:256
msgid "permissions"
@@ -3750,19 +3750,19 @@ msgstr "Nombre de usuario o contraseña inválidos."
#: taiga/users/services.py:590
msgid "You can't have more private projects"
-msgstr ""
+msgstr "No puedes tener más proyectos privados"
#: taiga/users/services.py:596
msgid "You can't have more public projects"
-msgstr ""
+msgstr "No puedes tener más proyectos públicos"
#: taiga/users/services.py:608
msgid "You have reached the limit of memberships for private projects"
-msgstr ""
+msgstr "Has alcanzado el límite de membresías para proyectos privados"
#: taiga/users/services.py:614
msgid "You have reached the limit of memberships for public projects"
-msgstr ""
+msgstr "As alcanzado el límite de membresías para proyectos públicos"
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
From 2b6678da0e39341ee66e53d58f35f7d5cf6510b5 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 28 Mar 2016 09:44:08 +0200
Subject: [PATCH 088/105] Fixing transfering email
---
taiga/projects/services/transfer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py
index 17b0bbb1..bef18577 100644
--- a/taiga/projects/services/transfer.py
+++ b/taiga/projects/services/transfer.py
@@ -50,7 +50,7 @@ def start_project_transfer(project, user, reason):
"token": token,
"reason": reason
}
- email = template(project.owner, context)
+ email = template(user, context)
email.send()
From a404cf9184602424565836213804f8725154893f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 28 Mar 2016 12:53:39 +0200
Subject: [PATCH 089/105] Fix text of seme emails
---
taiga/locale/ca/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/de/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/en/LC_MESSAGES/django.po | 69 +++++++------
taiga/locale/es/LC_MESSAGES/django.po | 96 +++++++++----------
taiga/locale/fi/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/fr/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/it/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/nl/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/pl/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/ru/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/sv/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/tr/LC_MESSAGES/django.po | 71 ++++++++------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 71 ++++++++------
.../emails/transfer_accept-body-html.jinja | 8 +-
.../emails/transfer_accept-body-text.jinja | 2 +-
.../emails/transfer_reject-body-html.jinja | 10 +-
.../emails/transfer_reject-body-text.jinja | 5 +-
.../emails/transfer_request-body-html.jinja | 6 +-
.../emails/transfer_start-body-html.jinja | 10 +-
.../emails/transfer_start-body-text.jinja | 3 +-
21 files changed, 628 insertions(+), 433 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 934e4f4a..aa2cfc20 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -2570,20 +2570,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2597,9 +2599,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2609,9 +2610,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2628,24 +2629,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2663,14 +2665,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2685,8 +2694,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2694,7 +2703,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2735,23 +2744,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2765,14 +2775,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 64a4be90..62632367 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -3078,20 +3078,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3105,9 +3107,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3117,9 +3118,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3136,24 +3137,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3171,14 +3173,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3193,8 +3202,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3202,7 +3211,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3243,23 +3252,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3273,14 +3283,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 7d3e8117..e0aced84 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-03-25 12:01+0100\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -2519,20 +2519,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2546,9 +2548,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2558,9 +2559,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2577,24 +2578,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2612,14 +2614,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2634,8 +2643,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2643,7 +2652,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2684,23 +2693,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2714,14 +2724,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index afcb7414..70f7506a 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -15,9 +15,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-27 11:05+0000\n"
-"Last-Translator: David Barragán \n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
"MIME-Version: 1.0\n"
@@ -2998,31 +2998,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
-"\n"
-" Hola %(old_owner_name)s,
\n"
-" %(new_owner_name)s ha aceptado tu proposición y será el nuevo dueño "
-"de \"%(project_name)s\".
\n"
-" "
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
-msgstr "Esta es la razón/comentario:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
+msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
-" "
-msgstr ""
-"\n"
-" De ahora en adelante, tu rol para este proyecto será de \"admin\"."
+"
From now on, your new status for this project will be \"admin\"."
"p>\n"
" "
+msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -3038,10 +3031,9 @@ msgstr ""
"\"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
-msgstr "Esta es la razón/comentario:"
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
msgid ""
@@ -3052,9 +3044,9 @@ msgstr ""
"De ahora en adelante, tu rol para este proyecto será de \"admin\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3075,36 +3067,27 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
-"\n"
-"Hola %(owner_name)s,
\n"
-"\n"
-"%(rejecter_name)s ha declinado tu oferta y no será el nuevo dueño del "
-"proyecto \"%(project_name)s\".
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
-"\n"
-"Esta es la razón/comentario:
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
-"\n"
-"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a "
-"una persona diferente.
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
@@ -3124,7 +3107,14 @@ msgstr ""
"%(rejecter_name)s ha declinado tu oferta y no será el dueño del nuevo "
"proyecto \"%(project_name)s\".\n"
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
@@ -3134,7 +3124,7 @@ msgstr ""
"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a "
"una persona diferente.\n"
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr "Solicitar transferir a una persona diferente:"
@@ -3152,8 +3142,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3161,7 +3151,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3202,23 +3192,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3232,14 +3223,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr "Aceptar o rechazar la transferencia de proyecto:"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index b72c130e..1b87a20d 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -2961,20 +2961,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2988,9 +2990,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3000,9 +3001,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3019,24 +3020,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3054,14 +3056,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3076,8 +3085,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3085,7 +3094,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3126,23 +3135,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3156,14 +3166,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index 299ca2a5..b640c23e 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -22,8 +22,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -2743,20 +2743,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2770,9 +2772,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2782,9 +2783,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2801,24 +2802,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2836,14 +2838,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2858,8 +2867,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2867,7 +2876,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2908,23 +2917,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2938,14 +2948,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index 0d55aa08..b658bc01 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,8 +14,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -3256,20 +3256,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3283,9 +3285,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3295,9 +3296,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3314,24 +3315,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3349,14 +3351,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3371,8 +3380,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3380,7 +3389,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3421,23 +3430,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3451,14 +3461,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index 675f8761..4e919b63 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -2654,20 +2654,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2681,9 +2683,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2693,9 +2694,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2712,24 +2713,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2747,14 +2749,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2769,8 +2778,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2778,7 +2787,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2819,23 +2828,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2849,14 +2859,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 842ab5d4..26721ac2 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -3020,20 +3020,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3047,9 +3049,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3059,9 +3060,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3078,24 +3079,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3113,14 +3115,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3135,8 +3144,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3144,7 +3153,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3185,23 +3194,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3215,14 +3225,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index d2f7b9af..ca7170f8 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
@@ -3003,20 +3003,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3030,9 +3032,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3042,9 +3043,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3061,24 +3062,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3096,14 +3098,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3118,8 +3127,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3127,7 +3136,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3168,23 +3177,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3198,14 +3208,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index 11e1e1e3..f5cfc47c 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -3021,20 +3021,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3048,9 +3050,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3060,9 +3061,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3079,24 +3080,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3114,14 +3116,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3136,8 +3145,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3145,7 +3154,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3186,23 +3195,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3216,14 +3226,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index f314b9b2..5e231065 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -2564,20 +2564,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2591,9 +2593,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2603,9 +2604,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2622,24 +2623,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2657,14 +2659,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2679,8 +2688,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2688,7 +2697,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2729,23 +2738,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2759,14 +2769,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 3bd5749e..21e93a5c 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -2749,20 +2749,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -2776,9 +2778,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -2788,9 +2789,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -2807,24 +2808,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -2842,14 +2844,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -2864,8 +2873,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -2873,7 +2882,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -2914,23 +2923,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -2944,14 +2954,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index cc2595bc..b735c7d7 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-03-25 12:01+0100\n"
-"PO-Revision-Date: 2016-03-25 11:01+0000\n"
+"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
@@ -2984,20 +2984,22 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(old_owner_name)s,
\n"
-" %(new_owner_name)s has accepted your offer and will become the new "
-"project owner for \"%(project_name)s\".
\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
-msgid "This is the reason/comment:
"
+#, python-format
+msgid "%(new_owner_name)s says:
"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
"\n"
-" From now on, your new status for this project will be \"admin\".
\n"
+" From now on, your new status for this project will be \"admin\"."
+"p>\n"
" "
msgstr ""
@@ -3011,9 +3013,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:6
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
-msgid "This is the reason/comment:"
+#, python-format
+msgid "%(new_owner_name)s says:"
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
@@ -3023,9 +3024,9 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:18
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
"\n"
"The Taiga Team\n"
@@ -3042,24 +3043,25 @@ msgstr ""
#, python-format
msgid ""
"\n"
-"
Hi %(owner_name)s,
\n"
-" %(rejecter_name)s has declined your offer and will not become the "
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(rejecter_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
msgid ""
"\n"
-" If you want, you can still try to transfer the project ownership to "
-"a different person.
\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
" "
msgstr ""
@@ -3077,14 +3079,21 @@ msgid ""
"project owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid ""
+"\n"
+"%(rejecter_name)s says:
\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
msgid "Request transfer to a different person:"
msgstr ""
@@ -3099,8 +3108,8 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(owner_name)s,
\n"
-" %(requester_name)s has requested to become the project owner for "
+"
Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".
\n"
" "
msgstr ""
@@ -3108,7 +3117,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
"\n"
-" Please, click on \"Continue\" if you would like to start the "
+"
Please, click on \"Continue\" if you would like to start the "
"project transfer from the administration panel.
\n"
" "
msgstr ""
@@ -3149,23 +3158,24 @@ msgstr ""
#, python-format
msgid ""
"\n"
-" Hi %(receiver_name)s,
\n"
-" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"
Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
"would like you to become the new project owner.
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
msgid ""
"\n"
-" This is the reason/comment:
\n"
+" %(owner_name)s says:
\n"
" "
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
"\n"
-" Please, click on \"Continue\" to either accept or reject this "
+"
Please, click on \"Continue\" to either accept or reject this "
"proposal.
\n"
" "
msgstr ""
@@ -3179,14 +3189,19 @@ msgid ""
"you to become the new project owner.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
"\n"
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_start-body-text.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project transfer:"
msgstr ""
diff --git a/taiga/projects/templates/emails/transfer_accept-body-html.jinja b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
index 2606af4b..6c8f409e 100644
--- a/taiga/projects/templates/emails/transfer_accept-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
@@ -2,16 +2,16 @@
{% block body %}
{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
- Hi {{old_owner_name}},
- {{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
+ Hi {{old_owner_name}},
+ {{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
{% endtrans %}
{% if reason %}
- {% trans %}This is the reason/comment:
{% endtrans %}
+ {% trans new_owner_name=new_owner.get_full_name()%}{{ new_owner_name }} says:
{% endtrans %}
{{reason}}
{% endif %}
{% trans %}
- From now on, your new status for this project will be "admin".
+ From now on, your new status for this project will be "admin".
{% endtrans %}
{% endblock %}
diff --git a/taiga/projects/templates/emails/transfer_accept-body-text.jinja b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
index 0d8779d8..a8034063 100644
--- a/taiga/projects/templates/emails/transfer_accept-body-text.jinja
+++ b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
@@ -3,7 +3,7 @@ Hi {{old_owner_name}},
{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
{% endtrans %}
-{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
+{% trans new_owner_name=new_owner.get_full_name()%}{{ new_owner_name }} says:{% endtrans %}
{{reason}}
{% endif %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-html.jinja b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
index ac17c5e7..6d54f322 100644
--- a/taiga/projects/templates/emails/transfer_reject-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
@@ -2,20 +2,20 @@
{% block body %}
{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
- Hi {{owner_name}},
- {{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}".
+ Hi {{ owner_name }},
+ {{ rejecter_name }} has declined your offer and will not become the new project owner for "{{project_name}}".
{% endtrans %}
{% if reason %}
- {% trans %}
- This is the reason/comment:
+ {% trans rejecter_name=rejecter.get_full_name()%}
+ {{ rejecter_name }} says:
{% endtrans %}
{{ reason }}
{% endif %}
{% trans %}
- If you want, you can still try to transfer the project ownership to a different person.
+ If you want, you can still try to transfer the project ownership to a different person.
{% endtrans %}
{{ rejecter_name }} says:
+{% endtrans %}
{{ reason }}
{% endif %}
diff --git a/taiga/projects/templates/emails/transfer_request-body-html.jinja b/taiga/projects/templates/emails/transfer_request-body-html.jinja
index 500e3433..3351c382 100644
--- a/taiga/projects/templates/emails/transfer_request-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_request-body-html.jinja
@@ -2,12 +2,12 @@
{% block body %}
{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
- Hi {{owner_name}},
- {{ requester_name }} has requested to become the project owner for "{{project_name}}".
+ Hi {{owner_name}},
+ {{ requester_name }} has requested to become the project owner for "{{project_name}}".
{% endtrans %}
{% trans %}
- Please, click on "Continue" if you would like to start the project transfer from the administration panel.
+ Please, click on "Continue" if you would like to start the project transfer from the administration panel.
{% endtrans %}
Hi {{receiver_name}},
- {{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.
+ Hi {{receiver_name}},
+ {{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.
{% endtrans %}
{% if reason %}
- {% trans %}
- This is the reason/comment:
+ {% trans owner_name=project.owner.get_full_name() %}
+ {{ owner_name }} says:
{% endtrans %}
{{ reason }}
{% endif %}
{% trans %}
- Please, click on "Continue" to either accept or reject this proposal.
+ Please, click on "Continue" to either accept or reject this proposal.
{% endtrans %}
Date: Mon, 28 Mar 2016 12:19:43 +0200
Subject: [PATCH 090/105] Checking max owner memberships when creating
invitations
---
taiga/projects/api.py | 2 +-
tests/integration/test_memberships.py | 27 +++++++++++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index ee8aa50d..a3fdbbeb 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -634,7 +634,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
members = len(data["bulk_memberships"])
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- request.user,
+ project.owner,
project=project,
members=members
)
diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py
index 3b9df18c..229ed205 100644
--- a/tests/integration/test_memberships.py
+++ b/tests/integration/test_memberships.py
@@ -77,6 +77,33 @@ def test_api_create_bulk_members_without_enough_memberships_private_project_slot
assert "reached the limit of memberships for private" in response.data["_error_message"]
+
+def test_api_create_bulk_members_for_admin_without_enough_memberships_private_project_slots_one_project(client):
+ owner = f.UserFactory.create(max_memberships_private_projects=3)
+ user = f.UserFactory.create()
+ project = f.ProjectFactory(owner=owner, is_private=True)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+
+ url = reverse("memberships-bulk-create")
+
+ data = {
+ "project_id": project.id,
+ "bulk_memberships": [
+ {"role_id": role.pk, "email": "test1@test.com"},
+ {"role_id": role.pk, "email": "test2@test.com"},
+ {"role_id": role.pk, "email": "test3@test.com"},
+ {"role_id": role.pk, "email": "test4@test.com"},
+ ]
+ }
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+
+ assert response.status_code == 400
+ assert "reached the limit of memberships for private" in response.data["_error_message"]
+
+
+
def test_api_create_bulk_members_with_enough_memberships_private_project_slots_multiple_projects(client):
user = f.UserFactory.create(max_memberships_private_projects=6)
project = f.ProjectFactory(owner=user, is_private=True)
From e5e61911ae8e27998a2c8d6c0f83982f66296eb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 29 Mar 2016 11:09:19 +0200
Subject: [PATCH 091/105] [i18n] update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 2 +-
taiga/locale/de/LC_MESSAGES/django.po | 2 +-
taiga/locale/en/LC_MESSAGES/django.po | 2 +-
taiga/locale/es/LC_MESSAGES/django.po | 2 +-
taiga/locale/fi/LC_MESSAGES/django.po | 2 +-
taiga/locale/fr/LC_MESSAGES/django.po | 2 +-
taiga/locale/it/LC_MESSAGES/django.po | 2 +-
taiga/locale/nl/LC_MESSAGES/django.po | 2 +-
taiga/locale/pl/LC_MESSAGES/django.po | 2 +-
taiga/locale/pt_BR/LC_MESSAGES/django.po | 2 +-
taiga/locale/ru/LC_MESSAGES/django.po | 2 +-
taiga/locale/sv/LC_MESSAGES/django.po | 2 +-
taiga/locale/tr/LC_MESSAGES/django.po | 2 +-
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 2 +-
14 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index aa2cfc20..6ccc7bde 100644
--- a/taiga/locale/ca/LC_MESSAGES/django.po
+++ b/taiga/locale/ca/LC_MESSAGES/django.po
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 62632367..bb13d44a 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/LC_MESSAGES/django.po
@@ -17,7 +17,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index e0aced84..0d77e9cb 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-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index 70f7506a..9ce8fbb2 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 1b87a20d..be8a9137 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/LC_MESSAGES/django.po
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index b640c23e..f6d46db7 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -22,7 +22,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index b658bc01..f0a2db80 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index 4e919b63..3213dacf 100644
--- a/taiga/locale/nl/LC_MESSAGES/django.po
+++ b/taiga/locale/nl/LC_MESSAGES/django.po
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 26721ac2..5b347587 100644
--- a/taiga/locale/pl/LC_MESSAGES/django.po
+++ b/taiga/locale/pl/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index ca7170f8..1d9b4cf3 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index f5cfc47c..d2ef7a6e 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 5e231065..04b355cf 100644
--- a/taiga/locale/sv/LC_MESSAGES/django.po
+++ b/taiga/locale/sv/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 21e93a5c..f423f4fc 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index b735c7d7..b7ad8a9f 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-28 12:50+0200\n"
+"POT-Creation-Date: 2016-03-29 11:08+0200\n"
"PO-Revision-Date: 2016-03-28 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
From d80f15b4eed447170221ec39549bd132c65a75c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 29 Mar 2016 12:12:37 +0200
Subject: [PATCH 092/105] Fix transfer emails templates
---
.../projects/templates/emails/transfer_accept-body-text.jinja | 1 +
.../projects/templates/emails/transfer_reject-body-html.jinja | 1 -
.../projects/templates/emails/transfer_reject-body-text.jinja | 4 +---
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/taiga/projects/templates/emails/transfer_accept-body-text.jinja b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
index a8034063..0fda1124 100644
--- a/taiga/projects/templates/emails/transfer_accept-body-text.jinja
+++ b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
@@ -3,6 +3,7 @@ Hi {{old_owner_name}},
{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
{% endtrans %}
+{% if reason %}
{% trans new_owner_name=new_owner.get_full_name()%}{{ new_owner_name }} says:{% endtrans %}
{{reason}}
{% endif %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-html.jinja b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
index 6d54f322..982915f7 100644
--- a/taiga/projects/templates/emails/transfer_reject-body-html.jinja
+++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
@@ -10,7 +10,6 @@
{% trans rejecter_name=rejecter.get_full_name()%}
{{ rejecter_name }} says:
{% endtrans %}
-
{{ reason }}
{% endif %}
diff --git a/taiga/projects/templates/emails/transfer_reject-body-text.jinja b/taiga/projects/templates/emails/transfer_reject-body-text.jinja
index 28f31a2c..972cce54 100644
--- a/taiga/projects/templates/emails/transfer_reject-body-text.jinja
+++ b/taiga/projects/templates/emails/transfer_reject-body-text.jinja
@@ -4,9 +4,7 @@ Hi {{owner_name}},
{% endtrans %}
{% if reason %}
-{% trans rejecter_name=rejecter.get_full_name()%}
-{{ rejecter_name }} says:
-{% endtrans %}
+{% trans rejecter_name=rejecter.get_full_name()%}{{ rejecter_name }} says:{% endtrans %}
{{ reason }}
{% endif %}
From 9f8a6aa4f5fc9ea11a517018f5a38e1ea7bc7cdc Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 29 Mar 2016 13:05:52 +0200
Subject: [PATCH 093/105] Fixing project transfer accept
---
taiga/projects/api.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index a3fdbbeb..2dd72282 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -378,13 +378,13 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
project = self.get_object()
self.check_permissions(request, "transfer_accept", project)
- members = project.memberships.count()
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
request.user,
project=project,
- members=members
+ members=0
)
if not enough_slots:
+ members = project.memberships.count()
raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
reason = request.DATA.get('reason', None)
From a1bd32054229fb73b8d0c96ca655f8980211d83f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20D=C3=B6rner?=
Date: Sun, 13 Mar 2016 13:14:49 +0100
Subject: [PATCH 094/105] Refactored occurrences of users.User to
get_user_model(), which allows a custom user model
---
AUTHORS.rst | 1 +
taiga/auth/services.py | 7 +--
taiga/auth/tokens.py | 4 +-
taiga/base/management/commands/test_emails.py | 22 ++++----
taiga/export_import/serializers.py | 3 +-
taiga/front/sitemaps/users.py | 3 +-
taiga/hooks/bitbucket/services.py | 4 +-
taiga/hooks/github/services.py | 4 +-
taiga/hooks/gitlab/services.py | 8 +--
taiga/mdrender/extensions/mentions.py | 7 ++-
taiga/projects/history/freeze_impl.py | 4 +-
taiga/projects/history/models.py | 6 +--
taiga/projects/issues/api.py | 6 +--
taiga/projects/likes/serializers.py | 8 ++-
taiga/projects/notifications/mixins.py | 16 ++----
taiga/projects/notifications/models.py | 6 +--
taiga/projects/notifications/serializers.py | 7 +--
taiga/projects/notifications/services.py | 4 +-
taiga/projects/services/stats.py | 4 +-
taiga/projects/votes/serializers.py | 8 ++-
taiga/stats/services.py | 3 +-
taiga/timeline/api.py | 9 ++--
taiga/timeline/apps.py | 3 +-
.../_rebuild_timeline_for_user_creation.py | 9 ++--
.../management/commands/rebuild_timeline.py | 9 +---
taiga/timeline/serializers.py | 3 +-
taiga/timeline/signals.py | 6 +--
taiga/users/models.py | 50 ++++++++++++++++---
taiga/users/services.py | 3 +-
taiga/webhooks/apps.py | 4 +-
tests/factories.py | 2 +-
tests/integration/test_importer_api.py | 1 -
tests/integration/test_watch_tasks.py | 1 -
tests/unit/test_slug.py | 8 +--
tests/unit/test_timeline.py | 7 ++-
35 files changed, 132 insertions(+), 118 deletions(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index b4a52014..2b829ce1 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -28,6 +28,7 @@ answer newbie questions, and generally made taiga that much better:
- Joe Letts
- Julien Palard
- luyikei
+- Motius GmbH
- Ricky Posner
- Yamila Moreno
- Yaser Alraddadi
diff --git a/taiga/auth/services.py b/taiga/auth/services.py
index 49f9ceaa..5015a02e 100644
--- a/taiga/auth/services.py
+++ b/taiga/auth/services.py
@@ -25,6 +25,7 @@ not uses clasess and uses simple functions.
"""
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.db import transaction as tx
from django.db import IntegrityError
from django.utils.translation import ugettext as _
@@ -69,7 +70,7 @@ def is_user_already_registered(*, username:str, email:str) -> (bool, str):
and in case he does whats the duplicated attribute
"""
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
if user_model.objects.filter(username=username):
return (True, _("Username is already in use."))
@@ -110,7 +111,7 @@ def public_register(username:str, password:str, email:str, full_name:str):
if is_registered:
raise exc.WrongArguments(reason)
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
user = user_model(username=username,
email=email,
full_name=full_name)
@@ -159,7 +160,7 @@ def private_register_for_new_user(token:str, username:str, email:str,
if is_registered:
raise exc.WrongArguments(reason)
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
user = user_model(username=username,
email=email,
full_name=full_name)
diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py
index 4d809e38..58fe2938 100644
--- a/taiga/auth/tokens.py
+++ b/taiga/auth/tokens.py
@@ -14,7 +14,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
+from django.contrib.auth import get_user_model
from taiga.base import exceptions as exc
from django.apps import apps
@@ -47,7 +47,7 @@ def get_user_for_token(token, scope, max_age=None):
except signing.BadSignature:
raise exc.NotAuthenticated(_("Invalid token"))
- model_cls = apps.get_model("users", "User")
+ model_cls = get_user_model()
try:
user = model_cls.objects.get(pk=data["user_%s_id" % (scope)])
diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py
index a3e99688..53a63a87 100644
--- a/taiga/base/management/commands/test_emails.py
+++ b/taiga/base/management/commands/test_emails.py
@@ -20,6 +20,7 @@ import datetime
from optparse import make_option
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.utils import timezone
@@ -28,7 +29,6 @@ from taiga.base.mails import mail_builder
from taiga.projects.models import Project, Membership
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.services import get_history_queryset_by_model_instance
-from taiga.users.models import User
class Command(BaseCommand):
@@ -50,7 +50,7 @@ class Command(BaseCommand):
# Register email
context = {"lang": locale,
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
"cancel_token": "cancel-token"}
email = mail_builder.registered_user(test_email, context)
@@ -58,7 +58,7 @@ class Command(BaseCommand):
# Membership invitation
membership = Membership.objects.order_by("?").filter(user__isnull=True).first()
- membership.invited_by = User.objects.all().order_by("?").first()
+ membership.invited_by = get_user_model().objects.all().order_by("?").first()
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
context = {"lang": locale, "membership": membership}
@@ -88,19 +88,19 @@ class Command(BaseCommand):
email.send()
# Password recovery
- context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
+ context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()}
email = mail_builder.password_recovery(test_email, context)
email.send()
# Change email
- context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
+ context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()}
email = mail_builder.change_email(test_email, context)
email.send()
# Export/Import emails
context = {
"lang": locale,
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
"error_subject": "Error generating project dump",
"error_message": "Error generating project dump",
@@ -109,7 +109,7 @@ class Command(BaseCommand):
email.send()
context = {
"lang": locale,
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
"error_subject": "Error importing project dump",
"error_message": "Error importing project dump",
}
@@ -120,7 +120,7 @@ class Command(BaseCommand):
context = {
"lang": locale,
"url": "http://dummyurl.com",
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
"deletion_date": deletion_date,
}
@@ -129,7 +129,7 @@ class Command(BaseCommand):
context = {
"lang": locale,
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
}
email = mail_builder.load_dump(test_email, context)
@@ -157,9 +157,9 @@ class Command(BaseCommand):
context = {
"lang": locale,
"project": Project.objects.all().order_by("?").first(),
- "changer": User.objects.all().order_by("?").first(),
+ "changer": get_user_model().objects.all().order_by("?").first(),
"history_entries": HistoryEntry.objects.all().order_by("?")[0:5],
- "user": User.objects.all().order_by("?").first(),
+ "user": get_user_model().objects.all().order_by("?").first(),
}
for notification_email in notification_emails:
diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py
index 49d574ca..55b2031d 100644
--- a/taiga/export_import/serializers.py
+++ b/taiga/export_import/serializers.py
@@ -21,6 +21,7 @@ import os
from collections import OrderedDict
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
@@ -263,7 +264,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer):
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
- User = apps.get_model("users", "User")
+ User = get_user_model()
adding_users = User.objects.filter(email__in=adding_watcher_emails)
removing_users = User.objects.filter(email__in=removing_watcher_emails)
diff --git a/taiga/front/sitemaps/users.py b/taiga/front/sitemaps/users.py
index 675839a0..5e956dd7 100644
--- a/taiga/front/sitemaps/users.py
+++ b/taiga/front/sitemaps/users.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
from django.apps import apps
+from django.contrib.auth import get_user_model
from taiga.front.templatetags.functions import resolve
@@ -24,7 +25,7 @@ from .base import Sitemap
class UsersSitemap(Sitemap):
def items(self):
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
# Only active users and not system users
queryset = user_model.objects.filter(is_active=True,
diff --git a/taiga/hooks/bitbucket/services.py b/taiga/hooks/bitbucket/services.py
index a1b12d9b..9b2b8d21 100644
--- a/taiga/hooks/bitbucket/services.py
+++ b/taiga/hooks/bitbucket/services.py
@@ -17,10 +17,10 @@
import uuid
+from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.conf import settings
-from taiga.users.models import User
from taiga.base.utils.urls import get_absolute_url
@@ -43,4 +43,4 @@ def get_or_generate_config(project):
def get_bitbucket_user(user_id):
- return User.objects.get(is_system=True, username__startswith="bitbucket")
+ return get_user_model().objects.get(is_system=True, username__startswith="bitbucket")
diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py
index b4114449..830406bf 100644
--- a/taiga/hooks/github/services.py
+++ b/taiga/hooks/github/services.py
@@ -17,9 +17,9 @@
import uuid
+from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
-from taiga.users.models import User
from taiga.users.models import AuthData
from taiga.base.utils.urls import get_absolute_url
@@ -49,6 +49,6 @@ def get_github_user(github_id):
pass
if user is None:
- user = User.objects.get(is_system=True, username__startswith="github")
+ user = get_user_model().objects.get(is_system=True, username__startswith="github")
return user
diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py
index 2b2af849..d80fe62c 100644
--- a/taiga/hooks/gitlab/services.py
+++ b/taiga/hooks/gitlab/services.py
@@ -17,10 +17,10 @@
import uuid
+from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.conf import settings
-from taiga.users.models import User
from taiga.base.utils.urls import get_absolute_url
@@ -47,11 +47,11 @@ def get_gitlab_user(user_email):
if user_email:
try:
- user = User.objects.get(email=user_email)
- except User.DoesNotExist:
+ user = get_user_model().objects.get(email=user_email)
+ except get_user_model().DoesNotExist:
pass
if user is None:
- user = User.objects.get(is_system=True, username__startswith="gitlab")
+ user = get_user_model().objects.get(is_system=True, username__startswith="gitlab")
return user
diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py
index 2dcea03a..683250d2 100644
--- a/taiga/mdrender/extensions/mentions.py
+++ b/taiga/mdrender/extensions/mentions.py
@@ -22,13 +22,12 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+from django.contrib.auth import get_user_model
from markdown.extensions import Extension
from markdown.inlinepatterns import Pattern
from markdown.util import etree, AtomicString
-from taiga.users.models import User
-
class MentionsExtension(Extension):
def extendMarkdown(self, md, md_globals):
@@ -43,8 +42,8 @@ class MentionsPattern(Pattern):
username = m.group(3)
try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
+ user = get_user_model().objects.get(username=username)
+ except get_user_model().DoesNotExist:
return "@{}".format(username)
url = "/profile/{}".format(username)
diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py
index 07de4bfc..34f4139d 100644
--- a/taiga/projects/history/freeze_impl.py
+++ b/taiga/projects/history/freeze_impl.py
@@ -19,10 +19,10 @@ from contextlib import suppress
from functools import partial
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
-from taiga.base.utils.urls import get_absolute_url
from taiga.base.utils.iterators import as_tuple
from taiga.base.utils.iterators import as_dict
from taiga.mdrender.service import render as mdrender
@@ -49,7 +49,7 @@ def _get_generic_values(ids:tuple, *, typename=None, attr:str="name") -> tuple:
@as_dict
def _get_users_values(ids:set) -> dict:
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
ids = filter(lambda x: x is not None, ids)
qs = user_model.objects.filter(pk__in=tuple(ids))
diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py
index 24526235..d440900c 100644
--- a/taiga/projects/history/models.py
+++ b/taiga/projects/history/models.py
@@ -14,12 +14,10 @@
import uuid
-from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.db import models
-from django.apps import apps
+from django.contrib.auth import get_user_model
from django.utils.functional import cached_property
-from django.conf import settings
from django_pgjson.fields import JsonField
from taiga.mdrender.service import get_diff_of_htmls
@@ -96,7 +94,7 @@ class HistoryEntry(models.Model):
@cached_property
def owner(self):
pk = self.user["pk"]
- model = apps.get_model("users", "User")
+ model = get_user_model()
try:
return model.objects.get(pk=pk)
except model.DoesNotExist:
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index 0a93250b..c0ade2f4 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -16,25 +16,21 @@
# along with this program. If not, see .
from django.utils.translation import ugettext as _
-from django.db.models import Q
from django.http import HttpResponse
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base import response
-from taiga.base.decorators import detail_route, list_route
+from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
-from taiga.users.models import User
-
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
-from taiga.projects.milestones.models import Milestone
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py
index ce321f24..7d28f8f8 100644
--- a/taiga/projects/likes/serializers.py
+++ b/taiga/projects/likes/serializers.py
@@ -16,16 +16,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api import serializers
-from taiga.base.fields import TagsField
+from django.contrib.auth import get_user_model
-from taiga.users.models import User
-from taiga.users.services import get_photo_or_gravatar_url
+from taiga.base.api import serializers
class FanSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
- model = User
+ model = get_user_model()
fields = ('id', 'username', 'full_name')
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 2751b2bd..fb3cada7 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -14,15 +14,11 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
from functools import partial
from operator import is_not
-from django.apps import apps
-from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
from taiga.base import response
from taiga.base.decorators import detail_route
@@ -34,8 +30,6 @@ from taiga.projects.notifications.utils import (attach_watchers_to_queryset,
attach_is_watcher_to_queryset,
attach_total_watchers_to_queryset)
-from taiga.users.models import User
-from . import models
from . serializers import WatcherSerializer
@@ -224,9 +218,9 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
- User = apps.get_model("users", "User")
- adding_users = User.objects.filter(id__in=adding_watcher_ids)
- removing_users = User.objects.filter(id__in=removing_watcher_ids)
+ User = get_user_model()
+ adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids)
+ removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
for user in adding_users:
services.add_watcher(obj, user)
@@ -273,7 +267,7 @@ class WatchersViewSetMixin:
try:
self.object = resource.get_watchers().get(pk=pk)
- except ObjectDoesNotExist: # or User.DoesNotExist
+ except ObjectDoesNotExist: # or User.DoesNotExist
return response.NotFound()
serializer = self.get_serializer(self.object)
diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py
index 8eb0db27..9c36fe75 100644
--- a/taiga/projects/notifications/models.py
+++ b/taiga/projects/notifications/models.py
@@ -33,7 +33,7 @@ class NotifyPolicy(models.Model):
project user notifications preference.
"""
project = models.ForeignKey("projects.Project", related_name="notify_policies")
- user = models.ForeignKey("users.User", related_name="notify_policies")
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notify_policies")
notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES)
created_at = models.DateTimeField(default=timezone.now)
@@ -57,7 +57,7 @@ class HistoryChangeNotification(models.Model):
or updated when an object requires notifications.
"""
key = models.CharField(max_length=255, unique=False, editable=False)
- owner = models.ForeignKey("users.User", null=False, blank=False,
+ owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
verbose_name=_("owner"), related_name="+")
created_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("created date time"))
@@ -66,7 +66,7 @@ class HistoryChangeNotification(models.Model):
history_entries = models.ManyToManyField("history.HistoryEntry",
verbose_name=_("history entries"),
related_name="+")
- notify_users = models.ManyToManyField("users.User",
+ notify_users = models.ManyToManyField(settings.AUTH_USER_MODEL,
verbose_name=_("notify users"),
related_name="+")
project = models.ForeignKey("projects.Project", null=False, blank=False,
diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py
index 8eab93a0..ed93dce7 100644
--- a/taiga/projects/notifications/serializers.py
+++ b/taiga/projects/notifications/serializers.py
@@ -15,13 +15,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import json
-
from taiga.base.api import serializers
-from taiga.users.models import User
+from taiga.users.models import get_user_model_safe
from . import models
-from . import choices
class NotifyPolicySerializer(serializers.ModelSerializer):
@@ -39,5 +36,5 @@ class WatcherSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
- model = User
+ model = get_user_model_safe()
fields = ('id', 'username', 'full_name')
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 9730f0c8..0a3cb8e7 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -20,7 +20,6 @@ import datetime
from functools import partial
from django.apps import apps
-from django.db.transaction import atomic
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
@@ -37,7 +36,6 @@ 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.users.models import User
from .models import HistoryChangeNotification, Watched
@@ -214,7 +212,7 @@ def send_notifications(obj, *, history):
return None
key = make_key_from_model_object(obj)
- owner = User.objects.get(pk=history.user["pk"])
+ owner = get_user_model().objects.get(pk=history.user["pk"])
notification, created = (HistoryChangeNotification.objects.select_for_update()
.get_or_create(key=key,
owner=owner,
diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py
index 6e12ff06..0972dc6b 100644
--- a/taiga/projects/services/stats.py
+++ b/taiga/projects/services/stats.py
@@ -22,8 +22,6 @@ import datetime
import copy
import collections
-from taiga.projects.history.models import HistoryEntry
-from taiga.projects.userstories.models import RolePoints
def _count_status_object(status_obj, counting_storage):
if status_obj.id in counting_storage:
@@ -228,6 +226,7 @@ def _get_milestones_stats_for_backlog(project, milestones):
def get_stats_for_project(project):
# Let's fetch all the estimations related to a project with all the necesary
# related data
+ RolePoints = apps.get_model('userstories', 'RolePoints')
role_points = RolePoints.objects.filter(
user_story__project = project,
).prefetch_related(
@@ -378,6 +377,7 @@ def _get_wiki_changes_per_member_stats(project):
# Wiki changes
wiki_changes = {}
wiki_page_keys = ["wiki.wikipage:%s"%id for id in project.wiki_pages.values_list("id", flat=True)]
+ HistoryEntry = apps.get_model('history', 'HistoryEntry')
history_entries = HistoryEntry.objects.filter(key__in=wiki_page_keys).values('user')
for entry in history_entries:
editions = wiki_changes.get(entry["user"]["pk"], 0)
diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py
index af9ffdfa..78fc94c4 100644
--- a/taiga/projects/votes/serializers.py
+++ b/taiga/projects/votes/serializers.py
@@ -16,16 +16,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api import serializers
-from taiga.base.fields import TagsField
+from django.contrib.auth import get_user_model
-from taiga.users.models import User
-from taiga.users.services import get_photo_or_gravatar_url
+from taiga.base.api import serializers
class VoterSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
- model = User
+ model = get_user_model()
fields = ('id', 'username', 'full_name')
diff --git a/taiga/stats/services.py b/taiga/stats/services.py
index 26934be3..45ae60e5 100644
--- a/taiga/stats/services.py
+++ b/taiga/stats/services.py
@@ -14,6 +14,7 @@
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.db.models import Count
from django.db.models import Q
from django.utils import timezone
@@ -27,7 +28,7 @@ from collections import OrderedDict
###########################################################################
def get_users_public_stats():
- model = apps.get_model("users", "User")
+ model = get_user_model()
queryset = model.objects.filter(is_active=True, is_system=False)
stats = OrderedDict()
diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py
index e68ba949..82dea784 100644
--- a/taiga/timeline/api.py
+++ b/taiga/timeline/api.py
@@ -14,7 +14,8 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
+from django.conf import settings
+from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.apps import apps
@@ -49,7 +50,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
page = self.paginate_queryset(queryset)
if page is not None:
user_ids = list(set([obj.data.get("user", {}).get("id", None) for obj in page.object_list]))
- User = apps.get_model("users", "User")
+ User = get_user_model()
users = {u.id: u for u in User.objects.filter(id__in=user_ids)}
for obj in page.object_list:
@@ -99,7 +100,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
class ProfileTimeline(TimelineViewSet):
- content_type = "users.user"
+ content_type = settings.AUTH_USER_MODEL.lower()
permission_classes = (permissions.UserTimelinePermission,)
def get_timeline(self, user):
@@ -107,7 +108,7 @@ class ProfileTimeline(TimelineViewSet):
class UserTimeline(TimelineViewSet):
- content_type = "users.user"
+ content_type = settings.AUTH_USER_MODEL.lower()
permission_classes = (permissions.UserTimelinePermission,)
def get_timeline(self, user):
diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py
index 3cdfbc8c..c9df776d 100644
--- a/taiga/timeline/apps.py
+++ b/taiga/timeline/apps.py
@@ -17,6 +17,7 @@
from django.apps import AppConfig
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.db.models import signals
@@ -35,4 +36,4 @@ class TimelineAppConfig(AppConfig):
signals.post_delete.connect(handlers.delete_membership_push_to_timeline,
sender=apps.get_model("projects", "Membership"))
signals.post_save.connect(handlers.create_user_push_to_timeline,
- sender=apps.get_model("users", "User"))
+ sender=get_user_model())
diff --git a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
index fdf8ef49..726664f9 100644
--- a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
+++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
@@ -18,24 +18,22 @@
# Examples:
# python manage.py rebuild_timeline_for_user_creation --settings=settings.local_timeline
-from django.conf import settings
+from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist
from django.core.management.base import BaseCommand
from django.db.models import Model
-from django.db import reset_queries
from django.test.utils import override_settings
from taiga.timeline.service import (_get_impl_key_from_model,
_timeline_impl_map, extract_user_info)
from taiga.timeline.models import Timeline
from taiga.timeline.signals import _push_to_timelines
-from taiga.users.models import User
from unittest.mock import patch
import gc
+
class BulkCreator(object):
def __init__(self):
self.timeline_objects = []
@@ -75,7 +73,7 @@ def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, c
def generate_timeline():
with patch('taiga.timeline.service._add_to_object_timeline', new=custom_add_to_object_timeline):
# Users api wasn't a HistoryResourceMixin so we can't interate on the HistoryEntries in this case
- users = User.objects.order_by("date_joined")
+ users = get_user_model().objects.order_by("date_joined")
for user in users.iterator():
print("User:", user.date_joined)
extra_data = {
@@ -87,6 +85,7 @@ def generate_timeline():
bulk_creator.flush()
+
class Command(BaseCommand):
help = 'Regenerate project timeline'
diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py
index 5bf7d340..6214d129 100644
--- a/taiga/timeline/management/commands/rebuild_timeline.py
+++ b/taiga/timeline/management/commands/rebuild_timeline.py
@@ -20,24 +20,17 @@
# python manage.py rebuild_timeline --settings=settings.local_timeline --purge
# python manage.py rebuild_timeline --settings=settings.local_timeline --initial_date 2014-10-02
-from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.core.management.base import BaseCommand
from django.db.models import Model
-from django.db import reset_queries
from django.test.utils import override_settings
-
from taiga.projects.models import Project
-from taiga.projects.history import services as history_services
-from taiga.projects.history.choices import HistoryType
from taiga.projects.history.models import HistoryEntry
from taiga.timeline.models import Timeline
-from taiga.timeline.service import (_add_to_object_timeline, _get_impl_key_from_model,
- _timeline_impl_map, extract_user_info)
+from taiga.timeline.service import _get_impl_key_from_model,_timeline_impl_map, extract_user_info
from taiga.timeline.signals import on_new_history_entry, _push_to_timelines
-from taiga.users.models import User
from unittest.mock import patch
from optparse import make_option
diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py
index 882f62bc..d4a1563c 100644
--- a/taiga/timeline/serializers.py
+++ b/taiga/timeline/serializers.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.forms import widgets
from taiga.base.api import serializers
@@ -37,7 +38,7 @@ class TimelineSerializer(serializers.ModelSerializer):
if hasattr(obj, "_prefetched_user"):
user = obj._prefetched_user
else:
- User = apps.get_model("users", "User")
+ User = get_user_model()
userData = obj.data.get("user", None)
try:
user = User.objects.get(id=userData["id"])
diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py
index f307fa4a..887688fc 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -16,14 +16,12 @@
# along with this program. If not, see .
from django.conf import settings
+from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.translation import ugettext as _
from taiga.projects.history import services as history_services
-from taiga.projects.models import Project
-from taiga.users.models import User
from taiga.projects.history.choices import HistoryType
-from taiga.projects.notifications import services as notifications_services
from taiga.timeline.service import (push_to_timeline,
build_user_namespace,
build_project_namespace,
@@ -93,7 +91,7 @@ def on_new_history_entry(sender, instance, created, **kwargs):
elif instance.type == HistoryType.delete:
event_type = "delete"
- user = User.objects.get(id=instance.user["pk"])
+ user = get_user_model().objects.get(id=instance.user["pk"])
values_diff = instance.values_diff
_clean_description_fields(values_diff)
diff --git a/taiga/users/models.py b/taiga/users/models.py
index 054167cc..bff7a025 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -15,19 +15,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from importlib import import_module
+
import random
import re
-import uuid
from django.apps import apps
+from django.apps.config import MODELS_MODULE_NAME
from django.conf import settings
+from django.contrib.auth.models import UserManager, AbstractBaseUser
from django.contrib.contenttypes.models import ContentType
+from django.core import validators
+from django.core.exceptions import AppRegistryNotReady
from django.db import models
from django.dispatch import receiver
-from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import UserManager, AbstractBaseUser
-from django.core import validators
from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
from django_pgjson.fields import JsonField
from djorm_pgarray.fields import TextArrayField
@@ -39,7 +42,42 @@ from taiga.permissions.permissions import MEMBERS_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel
-from easy_thumbnails.files import get_thumbnailer
+
+def get_user_model_safe():
+ """
+ Fetches the user model using the app registry.
+ This doesn't require that an app with the given app label exists,
+ which makes it safe to call when the registry is being populated.
+ All other methods to access models might raise an exception about the
+ registry not being ready yet.
+ Raises LookupError if model isn't found.
+
+ Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340
+ Ongoing Django issue: https://code.djangoproject.com/ticket/22872
+ """
+ user_app, user_model = settings.AUTH_USER_MODEL.split('.')
+
+ try:
+ return apps.get_model(user_app, user_model)
+ except AppRegistryNotReady:
+ if apps.apps_ready and not apps.models_ready:
+ # If this function is called while `apps.populate()` is
+ # loading models, ensure that the module that defines the
+ # target model has been imported and try looking the model up
+ # in the app registry. This effectively emulates
+ # `from path.to.app.models import Model` where we use
+ # `Model = get_model('app', 'Model')` instead.
+ app_config = apps.get_app_config(user_app)
+ # `app_config.import_models()` cannot be used here because it
+ # would interfere with `apps.populate()`.
+ import_module('%s.%s' % (app_config.name, MODELS_MODULE_NAME))
+ # In order to account for case-insensitivity of model_name,
+ # look up the model through a private API of the app registry.
+ return apps.get_registered_model(user_app, user_model)
+ else:
+ # This must be a different case (e.g. the model really doesn't
+ # exist). We just re-raise the exception.
+ raise
def generate_random_hex_color():
@@ -281,7 +319,7 @@ class Role(models.Model):
class AuthData(models.Model):
- user = models.ForeignKey("users.User", related_name="auth_data")
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="auth_data")
key = models.SlugField(max_length=50)
value = models.CharField(max_length=300)
extra = JsonField()
diff --git a/taiga/users/services.py b/taiga/users/services.py
index e13355ad..c55b4cdb 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -20,6 +20,7 @@ This model contains a domain logic for users application.
"""
from django.apps import apps
+from django.contrib.auth import get_user_model
from django.db.models import Q
from django.db import connection
from django.conf import settings
@@ -40,7 +41,7 @@ from .gravatar import get_gravatar_url
def get_user_by_username_or_email(username_or_email):
- user_model = apps.get_model("users", "User")
+ user_model = get_user_model()
qs = user_model.objects.filter(Q(username__iexact=username_or_email) |
Q(email__iexact=username_or_email))
diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py
index 90890262..a10cd1d2 100644
--- a/taiga/webhooks/apps.py
+++ b/taiga/webhooks/apps.py
@@ -27,8 +27,10 @@ def connect_webhooks_signals():
dispatch_uid="webhooks")
+
+
def disconnect_webhooks_signals():
- signals.post_save.disconnect(sender=HistoryEntry, dispatch_uid="webhooks")
+ signals.post_save.disconnect(sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="webhooks")
class WebhooksAppConfig(AppConfig):
diff --git a/tests/factories.py b/tests/factories.py
index 71de1044..252ce47a 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -165,7 +165,7 @@ class WikiAttachmentFactory(Factory):
class UserFactory(Factory):
class Meta:
- model = "users.User"
+ model = settings.AUTH_USER_MODEL
strategy = factory.CREATE_STRATEGY
username = factory.Sequence(lambda n: "user{}".format(n))
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 9d4e8d0b..2d392fbd 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -132,7 +132,6 @@ def test_valid_project_with_enough_public_projects_slots(client):
client.login(user)
response = client.json.post(url, json.dumps(data))
- print(response.content)
assert response.status_code == 201
assert Project.objects.filter(slug="public-project-with-slots").count() == 1
diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py
index 8c716560..38ddd40b 100644
--- a/tests/integration/test_watch_tasks.py
+++ b/tests/integration/test_watch_tasks.py
@@ -109,7 +109,6 @@ def test_get_task_is_watcher(client):
assert response.data['is_watcher'] == False
response = client.post(url_watch)
- print(response.data)
assert response.status_code == 200
response = client.get(url_detail)
diff --git a/tests/unit/test_slug.py b/tests/unit/test_slug.py
index 0bebf51f..9cb4ef5f 100644
--- a/tests/unit/test_slug.py
+++ b/tests/unit/test_slug.py
@@ -16,9 +16,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.projects.models import Project
-from taiga.users.models import User
+from django.contrib.auth import get_user_model
+from taiga.projects.models import Project
from taiga.base.utils.slug import slugify
import pytest
@@ -38,7 +38,7 @@ def test_slugify_3():
def test_project_slug_with_special_chars():
- user = User.objects.create(username="test")
+ user = get_user_model().objects.create(username="test")
project = Project.objects.create(name="漢字", description="漢字", owner=user)
project.save()
@@ -46,7 +46,7 @@ def test_project_slug_with_special_chars():
def test_project_with_existing_name_slug_with_special_chars():
- user = User.objects.create(username="test")
+ user = get_user_model().objects.create(username="test")
Project.objects.create(name="漢字", description="漢字", owner=user)
project = Project.objects.create(name="漢字", description="漢字", owner=user)
diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py
index afebbc09..e34906d6 100644
--- a/tests/unit/test_timeline.py
+++ b/tests/unit/test_timeline.py
@@ -18,19 +18,18 @@
from unittest.mock import patch, call
-from django.core.exceptions import ValidationError
+from django.contrib.auth import get_user_model
from taiga.timeline import service
from taiga.timeline.models import Timeline
from taiga.projects.models import Project
-from taiga.users.models import User
import pytest
def test_push_to_timeline_many_objects():
with patch("taiga.timeline.service._add_to_object_timeline") as mock:
- users = [User(), User(), User()]
+ users = [get_user_model(), get_user_model(), get_user_model()]
project = Project()
service.push_to_timeline(users, project, "test", project.created_date)
assert mock.call_count == 3
@@ -45,7 +44,7 @@ def test_push_to_timeline_many_objects():
def test_add_to_objects_timeline():
with patch("taiga.timeline.service._add_to_object_timeline") as mock:
- users = [User(), User(), User()]
+ users = [get_user_model(), get_user_model(), get_user_model()]
project = Project()
service._add_to_objects_timeline(users, project, "test", project.created_date)
assert mock.call_count == 3
From 0eb73ea07c296fd439fcc132d445a2779cc0db12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 30 Mar 2016 09:17:01 +0200
Subject: [PATCH 095/105] Fix lisence text
---
settings/local.py.example | 1 +
1 file changed, 1 insertion(+)
diff --git a/settings/local.py.example b/settings/local.py.example
index d0052032..e1bd9383 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -1,6 +1,7 @@
# 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
From 5e51ed4ee51269f4a4c57180c81528fab09fb959 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 30 Mar 2016 10:59:41 +0200
Subject: [PATCH 096/105] Updating project-admin url for taiga-front
---
taiga/front/urls.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/taiga/front/urls.py b/taiga/front/urls.py
index 499328f9..e7ea3e0f 100644
--- a/taiga/front/urls.py
+++ b/taiga/front/urls.py
@@ -48,6 +48,5 @@ urls = {
"project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token
- "project-admin": "/project/{0}/admin/project-profile/details", # project.slug
+ "project-admin": "/login?next=/project/{0}/admin/project-profile/details", # project.slug
}
-
From 7c505b70d9ee08e88218ba0157940801975205b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 30 Mar 2016 11:14:09 +0200
Subject: [PATCH 097/105] Make contrib plugins compatible with django 1.9
---
taiga/contrib_routers.py | 20 --------------------
taiga/projects/services/modules_config.py | 2 +-
taiga/urls.py | 2 --
3 files changed, 1 insertion(+), 23 deletions(-)
delete mode 100644 taiga/contrib_routers.py
diff --git a/taiga/contrib_routers.py b/taiga/contrib_routers.py
deleted file mode 100644
index 85b869dc..00000000
--- a/taiga/contrib_routers.py
+++ /dev/null
@@ -1,20 +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.base import routers
-
-router = routers.DefaultRouter(trailing_slash=False)
diff --git a/taiga/projects/services/modules_config.py b/taiga/projects/services/modules_config.py
index 46cf6b9a..b850be56 100644
--- a/taiga/projects/services/modules_config.py
+++ b/taiga/projects/services/modules_config.py
@@ -24,7 +24,7 @@ from django.conf import settings
def get_modules_config(project):
modules_config, created = models.ProjectModulesConfig.objects.get_or_create(project=project)
- if created:
+ if created or modules_config.config == None:
modules_config.config = {}
for key, configurator_function_name in settings.PROJECT_MODULES_CONFIGURATORS.items():
diff --git a/taiga/urls.py b/taiga/urls.py
index 14eaf3a4..23afa70f 100644
--- a/taiga/urls.py
+++ b/taiga/urls.py
@@ -20,7 +20,6 @@ from django.conf.urls import patterns, include, url
from django.contrib import admin
from .routers import router
-from .contrib_routers import router as contrib_router
##############################################
@@ -29,7 +28,6 @@ from .contrib_routers import router as contrib_router
urlpatterns = [
url(r'^api/v1/', include(router.urls)),
- url(r'^api/v1/', include(contrib_router.urls)),
url(r'^api/v1/api-auth/', include('taiga.base.api.urls', namespace='api')),
url(r'^admin/', include(admin.site.urls)),
]
From fab3c1f1992a2c4f824c31329b9dd40b8f58e3d5 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 30 Mar 2016 12:29:12 +0200
Subject: [PATCH 098/105] moving total_memberships to project detail serializer
---
taiga/projects/serializers.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 11720532..eec16301 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -344,6 +344,7 @@ class ProjectDetailSerializer(ProjectSerializer):
roles = ProjectRoleSerializer(source="roles", many=True, read_only=True)
members = serializers.SerializerMethodField(method_name="get_members")
+ total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
def get_members(self, obj):
qs = obj.memberships.filter(user__isnull=False)
@@ -353,11 +354,13 @@ class ProjectDetailSerializer(ProjectSerializer):
serializer = ProjectMemberSerializer(qs, many=True)
return serializer.data
+ def get_total_memberships(self, obj):
+ return services.get_total_project_memberships(obj)
+
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
- total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
class Meta:
model = models.Project
@@ -370,8 +373,6 @@ class ProjectDetailAdminSerializer(ProjectDetailSerializer):
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
- def get_total_memberships(self, obj):
- return services.get_total_project_memberships(obj)
From 6aecf282d1e606616c0adb5d66ccd9a68b907b41 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 30 Mar 2016 13:06:58 +0200
Subject: [PATCH 099/105] Updating translations
---
taiga/base/exceptions.py | 2 +-
taiga/locale/ca/LC_MESSAGES/django.po | 169 ++++++++-------
taiga/locale/de/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/en/LC_MESSAGES/django.po | 162 +++++++--------
taiga/locale/es/LC_MESSAGES/django.po | 196 +++++++++---------
taiga/locale/fi/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/fr/LC_MESSAGES/django.po | 169 ++++++++-------
taiga/locale/it/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/nl/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/pl/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/ru/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/sv/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/tr/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 167 ++++++++-------
taiga/projects/api.py | 2 +-
taiga/projects/choices.py | 6 +-
.../migrations/0039_auto_20160322_1157.py | 2 +-
taiga/projects/serializers.py | 7 +-
.../emails/transfer_start-body-text.jinja | 2 +-
.../migrations/0015_auto_20160120_1409.py | 4 +-
taiga/users/models.py | 4 +-
taiga/users/services.py | 4 +-
tests/integration/test_importer_api.py | 8 +-
tests/integration/test_memberships.py | 10 +-
tests/integration/test_projects.py | 2 +-
26 files changed, 1201 insertions(+), 1218 deletions(-)
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index 75b15bd2..104ba896 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -214,7 +214,7 @@ class NotEnoughSlotsForProject(BaseException):
Exception used on import/edition/creation project errors where the user
hasn't slots enough
"""
- default_detail = _("Not enough slots for project.")
+ default_detail = _("No room left for more projects.")
def __init__(self, is_private, total_memberships, detail=None):
self.detail = detail or self.default_detail
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 6ccc7bde..8d3eeb41 100644
--- a/taiga/locale/ca/LC_MESSAGES/django.po
+++ b/taiga/locale/ca/LC_MESSAGES/django.po
@@ -3,15 +3,16 @@
# This file is distributed under the same license as the taiga-back package.
#
# Translators:
-# Javier Julián Olmos , 2015
+# Xaviju , 2015
# Taiga Dev Team , 2015
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-29 11:08+0200\n"
-"PO-Revision-Date: 2016-03-28 10:50+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-03-30 12:58+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
"MIME-Version: 1.0\n"
@@ -41,27 +42,27 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "El mot d'usuari ja està en ús."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Aquest e-mail ja està en ús."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "El token no s'ajusta a cap invitació vàlida"
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Aquest usuari ja està registrat"
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr ""
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Error creant un nou usuari."
@@ -185,7 +186,7 @@ msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
#: taiga/hooks/api.py:68 taiga/projects/api.py:629
-#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: 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
@@ -341,7 +342,7 @@ msgid "Precondition error"
msgstr "Precondició errònia."
#: taiga/base/exceptions.py:217
-msgid "Not enough slots for project."
+msgid "No room left for more projects."
msgstr ""
#: taiga/base/filters.py:79 taiga/base/filters.py:444
@@ -536,21 +537,21 @@ msgstr ""
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/serializers.py:177
+#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr ""
-#: taiga/export_import/serializers.py:442
+#: taiga/export_import/serializers.py:443
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:457
+#: taiga/export_import/serializers.py:458
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Conté camps personalitzats invàlids."
-#: taiga/export_import/serializers.py:527
+#: taiga/export_import/serializers.py:528
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -722,7 +723,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nom"
@@ -760,11 +761,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:100
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr "Nom complet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:95
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "Adreça d'email"
@@ -848,7 +849,7 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "El payload no és un arxiu json vàlid"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El projecte no existeix"
@@ -1172,7 +1173,7 @@ msgid "The user doesn't exist"
msgstr ""
#: taiga/projects/api.py:366
-msgid "The user must be a member of the project"
+msgid "The user must be already a project member"
msgstr ""
#: taiga/projects/api.py:668
@@ -1211,7 +1212,7 @@ msgstr "Amo"
#: taiga/projects/notifications/models.py:73
#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302
msgid "project"
msgstr "Projecte"
@@ -1250,7 +1251,7 @@ msgstr "està obsolet "
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "Ordre"
@@ -1271,15 +1272,15 @@ msgid "Talky"
msgstr ""
#: taiga/projects/choices.py:32
-msgid "This project was blocked by nonpayment"
+msgid "This project is blocked due to payment failure"
msgstr ""
#: taiga/projects/choices.py:33
-msgid "This project was blocked by staff"
+msgid "This project is blocked by admin staff"
msgstr ""
#: taiga/projects/choices.py:34
-msgid "This project was blocked because the owner left"
+msgid "This project is blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
@@ -1396,7 +1397,7 @@ msgstr "Borrat"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Sense assignar"
@@ -1457,23 +1458,23 @@ msgstr "nota de bloqueig"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:158
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tens permissos per a ficar aquest sprint a aquesta incidència"
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this status to this issue."
msgstr "No tens permissos per a ficar aquest status a aquesta tasca"
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tens permissos per a ficar aquesta severitat a aquesta tasca"
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tens permissos per a ficar aquesta prioritat a aquesta incidència"
-#: taiga/projects/issues/api.py:178
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "No tens permissos per a ficar aquest tipus a aquesta incidència"
@@ -1530,7 +1531,7 @@ msgstr "Fans"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
msgid "slug"
msgstr "slug"
@@ -1580,7 +1581,7 @@ msgstr "email"
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:82 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
@@ -1748,7 +1749,7 @@ msgstr "està arxivat"
#: taiga/projects/models.py:482 taiga/projects/models.py:544
#: taiga/projects/models.py:577 taiga/projects/models.py:600
#: taiga/projects/models.py:627 taiga/projects/models.py:658
-#: taiga/users/models.py:102
+#: taiga/users/models.py:140
msgid "color"
msgstr "color"
@@ -1830,12 +1831,12 @@ msgstr ""
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:66
-#: taiga/projects/notifications/services.py:80
+#: taiga/projects/notifications/services.py:64
+#: taiga/projects/notifications/services.py:78
msgid "Notify exists for specified user and project"
msgstr ""
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr ""
@@ -2350,11 +2351,11 @@ msgid "Invalid role for the project"
msgstr "Rol invàlid per al projecte"
#: taiga/projects/serializers.py:195
-msgid "Project owner must be admin."
+msgid "The project owner must be admin."
msgstr ""
#: taiga/projects/serializers.py:198
-msgid "In this project at least one of the users must be an active admin."
+msgid "At least one user must be an active admin for this project."
msgstr ""
#: taiga/projects/serializers.py:394
@@ -2393,11 +2394,11 @@ msgstr "Severitats"
msgid "Roles"
msgstr "Rols"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:218
+#: taiga/projects/services/stats.py:216
msgid "Project End"
msgstr ""
@@ -2598,19 +2599,19 @@ msgid ""
"owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
@@ -2643,7 +2644,7 @@ msgid ""
" "
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
"\n"
" If you want, you can still try to transfer the project ownership to a "
@@ -2651,8 +2652,8 @@ msgid ""
" "
msgstr ""
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
msgid "Request transfer to a different person"
msgstr ""
@@ -2667,19 +2668,17 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
-msgid ""
-"\n"
-"
%(rejecter_name)s says:
\n"
+msgid "%(rejecter_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
msgid "Request transfer to a different person:"
msgstr ""
@@ -2788,7 +2787,7 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
-msgid "Accept or reject the project transfer:"
+msgid "Accept or reject the project ownership transfer:"
msgstr ""
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
@@ -3113,7 +3112,7 @@ msgstr "últim a modificar"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -3175,11 +3174,11 @@ msgstr ""
msgid "Invalid, are you sure the token is correct?"
msgstr "Invàlid. Estás segur que el token es correcte?"
-#: taiga/users/models.py:58
+#: taiga/users/models.py:96
msgid "superuser status"
msgstr "estatus de superusuari"
-#: taiga/users/models.py:59
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3187,24 +3186,24 @@ msgstr ""
"Designa que aquest usuari te tots els permisos sense asignarli-los "
"explícitament."
-#: taiga/users/models.py:89
+#: taiga/users/models.py:127
msgid "username"
msgstr "mot d'usuari"
-#: taiga/users/models.py:90
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Requerit. 30 caràcters o menys. Lletres, nombres i caràcters /./-/_"
-#: taiga/users/models.py:93
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Introdueix un nom d'usuari vàlid"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:134
msgid "active"
msgstr "actiu"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3212,59 +3211,59 @@ msgstr ""
"Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en "
"lloc de borrar el compte."
-#: taiga/users/models.py:103
+#: taiga/users/models.py:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "data d'unió"
-#: taiga/users/models.py:109
+#: taiga/users/models.py:147
msgid "default language"
msgstr "llenguatge per defecte"
-#: taiga/users/models.py:111
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:113
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "zona horaria per defecte"
-#: taiga/users/models.py:115
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "coloritza tags"
-#: taiga/users/models.py:120
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token de correu"
-#: taiga/users/models.py:122
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nova adreça de correu"
-#: taiga/users/models.py:129
-msgid "max number of private projects owned"
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:132
-msgid "max number of public projects owned"
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:135
+#: taiga/users/models.py:173
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:139
+#: taiga/users/models.py:177
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:256
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permissos"
@@ -3276,24 +3275,24 @@ msgstr "invàlid"
msgid "Invalid username. Try with a different one."
msgstr "Nom d'usuari invàlid"
-#: taiga/users/services.py:52 taiga/users/services.py:69
+#: taiga/users/services.py:53 taiga/users/services.py:70
msgid "Username or password does not matches user."
msgstr ""
-#: taiga/users/services.py:590
+#: taiga/users/services.py:591
msgid "You can't have more private projects"
msgstr ""
-#: taiga/users/services.py:596
+#: taiga/users/services.py:597
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:608
-msgid "You have reached the limit of memberships for private projects"
+#: taiga/users/services.py:609
+msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/users/services.py:614
-msgid "You have reached the limit of memberships for public projects"
+#: taiga/users/services.py:615
+msgid "You have reached your current limit of memberships for public projects"
msgstr ""
#: taiga/users/templates/emails/change_email-body-html.jinja:4
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index bb13d44a..182d0978 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/LC_MESSAGES/django.po
@@ -17,9 +17,10 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-29 11:08+0200\n"
-"PO-Revision-Date: 2016-03-28 10:50+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-03-30 12:58+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
"MIME-Version: 1.0\n"
@@ -51,27 +52,27 @@ msgstr ""
"255 oder weniger Zeichen aus Buchstaben, Zahlen und Punkt, Minus oder "
"Unterstrich erforderlich."
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Der Benutzername wird schon verwendet."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Diese E-Mail Adresse wird schon verwendet."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Der Benutzer ist schon registriert."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr ""
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Fehler bei der Erstellung des neuen Benutzers."
@@ -215,7 +216,7 @@ msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
#: taiga/hooks/api.py:68 taiga/projects/api.py:629
-#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: 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
@@ -373,7 +374,7 @@ msgid "Precondition error"
msgstr "Voraussetzungsfehler"
#: taiga/base/exceptions.py:217
-msgid "Not enough slots for project."
+msgid "No room left for more projects."
msgstr ""
#: taiga/base/filters.py:79 taiga/base/filters.py:444
@@ -591,21 +592,21 @@ msgstr "Fehler beim Importieren der Schlagworte"
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
-#: taiga/export_import/serializers.py:177
+#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden"
-#: taiga/export_import/serializers.py:442
+#: taiga/export_import/serializers.py:443
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}"
-#: taiga/export_import/serializers.py:457
+#: taiga/export_import/serializers.py:458
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Enthält ungültige Benutzerfelder."
-#: taiga/export_import/serializers.py:527
+#: taiga/export_import/serializers.py:528
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Der Name für das Projekt ist doppelt vergeben"
@@ -870,7 +871,7 @@ msgstr "Authentifizierung erforderlich"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "Name"
@@ -908,11 +909,11 @@ msgstr "Benutzer"
msgid "application"
msgstr "Applikation"
-#: taiga/feedback/models.py:24 taiga/users/models.py:100
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr "vollständiger Name"
-#: taiga/feedback/models.py:26 taiga/users/models.py:95
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "E-Mail Adresse"
@@ -996,7 +997,7 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "Die Nutzlast ist kein gültiges json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Das Projekt existiert nicht"
@@ -1362,7 +1363,7 @@ msgid "The user doesn't exist"
msgstr ""
#: taiga/projects/api.py:366
-msgid "The user must be a member of the project"
+msgid "The user must be already a project member"
msgstr ""
#: taiga/projects/api.py:668
@@ -1401,7 +1402,7 @@ msgstr "Besitzer"
#: taiga/projects/notifications/models.py:73
#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302
msgid "project"
msgstr "Projekt"
@@ -1440,7 +1441,7 @@ msgstr "wurde verworfen"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "Reihenfolge"
@@ -1461,15 +1462,15 @@ msgid "Talky"
msgstr "Gesprächig"
#: taiga/projects/choices.py:32
-msgid "This project was blocked by nonpayment"
+msgid "This project is blocked due to payment failure"
msgstr ""
#: taiga/projects/choices.py:33
-msgid "This project was blocked by staff"
+msgid "This project is blocked by admin staff"
msgstr ""
#: taiga/projects/choices.py:34
-msgid "This project was blocked because the owner left"
+msgid "This project is blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
@@ -1586,7 +1587,7 @@ msgstr "entfernt"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Nicht zugewiesen"
@@ -1647,27 +1648,27 @@ msgstr "Blockierungsgrund"
msgid "sprint"
msgstr "Sprint"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:158
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Sprint zu setzen."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this status to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Status zu setzen. "
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Gewichtung zu setzen."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Priorität zu setzen. "
-#: taiga/projects/issues/api.py:178
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "Sie haben nicht die Berechtigung, das Ticket auf diese Art zu setzen."
@@ -1724,7 +1725,7 @@ msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
msgid "slug"
msgstr "Slug"
@@ -1774,7 +1775,7 @@ msgstr "E-Mail"
msgid "create at"
msgstr "erstellt am "
-#: taiga/projects/models.py:82 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "Token"
@@ -1942,7 +1943,7 @@ msgstr "ist archiviert"
#: taiga/projects/models.py:482 taiga/projects/models.py:544
#: taiga/projects/models.py:577 taiga/projects/models.py:600
#: taiga/projects/models.py:627 taiga/projects/models.py:658
-#: taiga/users/models.py:102
+#: taiga/users/models.py:140
msgid "color"
msgstr "Farbe"
@@ -2024,12 +2025,12 @@ msgstr "Benutzer benachrichtigen"
msgid "Watched"
msgstr "Beobachtet"
-#: taiga/projects/notifications/services.py:66
-#: taiga/projects/notifications/services.py:80
+#: taiga/projects/notifications/services.py:64
+#: taiga/projects/notifications/services.py:78
msgid "Notify exists for specified user and project"
msgstr "Benachrichtigung für bestimmte Benutzer und Projekt aktiviert"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Ungültiger Wert für Benachrichtigungslevel"
@@ -2822,11 +2823,11 @@ msgid "Invalid role for the project"
msgstr "Ungültige Rolle für dieses Projekt"
#: taiga/projects/serializers.py:195
-msgid "Project owner must be admin."
+msgid "The project owner must be admin."
msgstr ""
#: taiga/projects/serializers.py:198
-msgid "In this project at least one of the users must be an active admin."
+msgid "At least one user must be an active admin for this project."
msgstr ""
#: taiga/projects/serializers.py:394
@@ -2865,11 +2866,11 @@ msgstr "Gewichtung"
msgid "Roles"
msgstr "Rollen"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
-#: taiga/projects/services/stats.py:218
+#: taiga/projects/services/stats.py:216
msgid "Project End"
msgstr "Projektende"
@@ -3106,19 +3107,19 @@ msgid ""
"owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
@@ -3151,7 +3152,7 @@ msgid ""
" "
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
"\n"
" If you want, you can still try to transfer the project ownership to a "
@@ -3159,8 +3160,8 @@ msgid ""
" "
msgstr ""
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
msgid "Request transfer to a different person"
msgstr ""
@@ -3175,19 +3176,17 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
-msgid ""
-"\n"
-"
%(rejecter_name)s says:
\n"
+msgid "%(rejecter_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
msgid "Request transfer to a different person:"
msgstr ""
@@ -3296,7 +3295,7 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
-msgid "Accept or reject the project transfer:"
+msgid "Accept or reject the project ownership transfer:"
msgstr ""
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
@@ -3636,7 +3635,7 @@ msgstr "letzte Änderung"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Prüfe die API der Historie auf Übereinstimmung"
@@ -3699,11 +3698,11 @@ msgstr ""
msgid "Invalid, are you sure the token is correct?"
msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?"
-#: taiga/users/models.py:58
+#: taiga/users/models.py:96
msgid "superuser status"
msgstr "Superuser Status"
-#: taiga/users/models.py:59
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3711,25 +3710,25 @@ msgstr ""
"Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor "
"zugewiesen werden müssen. "
-#: taiga/users/models.py:89
+#: taiga/users/models.py:127
msgid "username"
msgstr "Benutzername"
-#: taiga/users/models.py:90
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Benötigt. 30 Zeichen oder weniger.. Buchstaben, Zahlen und /./-/_ Zeichen"
-#: taiga/users/models.py:93
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Geben Sie einen gültigen Benuzternamen ein."
-#: taiga/users/models.py:96
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3737,59 +3736,59 @@ msgstr ""
"Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen "
"Benutzer zu löschen."
-#: taiga/users/models.py:103
+#: taiga/users/models.py:141
msgid "biography"
msgstr "Über mich"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:144
msgid "photo"
msgstr "Foto"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "Beitrittsdatum"
-#: taiga/users/models.py:109
+#: taiga/users/models.py:147
msgid "default language"
msgstr "Vorgegebene Sprache"
-#: taiga/users/models.py:111
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "Standard-Theme"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "Vorgegebene Zeitzone"
-#: taiga/users/models.py:115
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "Tag-Farben"
-#: taiga/users/models.py:120
+#: taiga/users/models.py:158
msgid "email token"
msgstr "E-Mail Token"
-#: taiga/users/models.py:122
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "neue E-Mail Adresse"
-#: taiga/users/models.py:129
-msgid "max number of private projects owned"
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:132
-msgid "max number of public projects owned"
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:135
+#: taiga/users/models.py:173
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:139
+#: taiga/users/models.py:177
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:256
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "Berechtigungen"
@@ -3801,24 +3800,24 @@ msgstr "ungültig"
msgid "Invalid username. Try with a different one."
msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen."
-#: taiga/users/services.py:52 taiga/users/services.py:69
+#: taiga/users/services.py:53 taiga/users/services.py:70
msgid "Username or password does not matches user."
msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein."
-#: taiga/users/services.py:590
+#: taiga/users/services.py:591
msgid "You can't have more private projects"
msgstr ""
-#: taiga/users/services.py:596
+#: taiga/users/services.py:597
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:608
-msgid "You have reached the limit of memberships for private projects"
+#: taiga/users/services.py:609
+msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/users/services.py:614
-msgid "You have reached the limit of memberships for public projects"
+#: taiga/users/services.py:615
+msgid "You have reached your current limit of memberships for public projects"
msgstr ""
#: taiga/users/templates/emails/change_email-body-html.jinja:4
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 0d77e9cb..47b5204e 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-03-29 11:08+0200\n"
+"POT-Creation-Date: 2016-03-30 12:58+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -37,27 +37,27 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr ""
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr ""
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr ""
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr ""
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr ""
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr ""
@@ -177,7 +177,7 @@ msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
#: taiga/hooks/api.py:68 taiga/projects/api.py:629
-#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: 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
@@ -333,7 +333,7 @@ msgid "Precondition error"
msgstr ""
#: taiga/base/exceptions.py:217
-msgid "Not enough slots for project."
+msgid "No room left for more projects."
msgstr ""
#: taiga/base/filters.py:79 taiga/base/filters.py:444
@@ -525,21 +525,21 @@ msgstr ""
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/serializers.py:177
+#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr ""
-#: taiga/export_import/serializers.py:442
+#: taiga/export_import/serializers.py:443
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr ""
-#: taiga/export_import/serializers.py:457
+#: taiga/export_import/serializers.py:458
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr ""
-#: taiga/export_import/serializers.py:527
+#: taiga/export_import/serializers.py:528
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr ""
@@ -711,7 +711,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr ""
@@ -749,11 +749,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:100
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr ""
-#: taiga/feedback/models.py:26 taiga/users/models.py:95
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr ""
@@ -821,7 +821,7 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr ""
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr ""
@@ -1145,7 +1145,7 @@ msgid "The user doesn't exist"
msgstr ""
#: taiga/projects/api.py:366
-msgid "The user must be a member of the project"
+msgid "The user must be already a project member"
msgstr ""
#: taiga/projects/api.py:668
@@ -1184,7 +1184,7 @@ msgstr ""
#: taiga/projects/notifications/models.py:73
#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302
msgid "project"
msgstr ""
@@ -1223,7 +1223,7 @@ msgstr ""
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr ""
@@ -1244,15 +1244,15 @@ msgid "Talky"
msgstr ""
#: taiga/projects/choices.py:32
-msgid "This project was blocked by nonpayment"
+msgid "This project is blocked due to payment failure"
msgstr ""
#: taiga/projects/choices.py:33
-msgid "This project was blocked by staff"
+msgid "This project is blocked by admin staff"
msgstr ""
#: taiga/projects/choices.py:34
-msgid "This project was blocked because the owner left"
+msgid "This project is blocked because the owner left"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
@@ -1369,7 +1369,7 @@ msgstr ""
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr ""
@@ -1430,23 +1430,23 @@ msgstr ""
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:158
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this status to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:178
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr ""
@@ -1503,7 +1503,7 @@ msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
msgid "slug"
msgstr ""
@@ -1553,7 +1553,7 @@ msgstr ""
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:82 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr ""
@@ -1721,7 +1721,7 @@ msgstr ""
#: taiga/projects/models.py:482 taiga/projects/models.py:544
#: taiga/projects/models.py:577 taiga/projects/models.py:600
#: taiga/projects/models.py:627 taiga/projects/models.py:658
-#: taiga/users/models.py:102
+#: taiga/users/models.py:140
msgid "color"
msgstr ""
@@ -1803,12 +1803,12 @@ msgstr ""
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:66
-#: taiga/projects/notifications/services.py:80
+#: taiga/projects/notifications/services.py:64
+#: taiga/projects/notifications/services.py:78
msgid "Notify exists for specified user and project"
msgstr ""
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr ""
@@ -2317,11 +2317,11 @@ msgid "Invalid role for the project"
msgstr ""
#: taiga/projects/serializers.py:195
-msgid "Project owner must be admin."
+msgid "The project owner must be admin."
msgstr ""
#: taiga/projects/serializers.py:198
-msgid "In this project at least one of the users must be an active admin."
+msgid "At least one user must be an active admin for this project."
msgstr ""
#: taiga/projects/serializers.py:394
@@ -2360,11 +2360,11 @@ msgstr ""
msgid "Roles"
msgstr ""
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:218
+#: taiga/projects/services/stats.py:216
msgid "Project End"
msgstr ""
@@ -2547,19 +2547,19 @@ msgid ""
"owner for \"%(project_name)s\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
@@ -2592,7 +2592,7 @@ msgid ""
" "
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
"\n"
" If you want, you can still try to transfer the project ownership to a "
@@ -2600,8 +2600,8 @@ msgid ""
" "
msgstr ""
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
msgid "Request transfer to a different person"
msgstr ""
@@ -2616,19 +2616,17 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
-msgid ""
-"\n"
-"
%(rejecter_name)s says:
\n"
+msgid "%(rejecter_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
msgid "Request transfer to a different person:"
msgstr ""
@@ -2737,7 +2735,7 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
-msgid "Accept or reject the project transfer:"
+msgid "Accept or reject the project ownership transfer:"
msgstr ""
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
@@ -3062,7 +3060,7 @@ msgstr ""
msgid "href"
msgstr ""
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -3123,92 +3121,92 @@ msgstr ""
msgid "Invalid, are you sure the token is correct?"
msgstr ""
-#: taiga/users/models.py:58
+#: taiga/users/models.py:96
msgid "superuser status"
msgstr ""
-#: taiga/users/models.py:59
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:89
+#: taiga/users/models.py:127
msgid "username"
msgstr ""
-#: taiga/users/models.py:90
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
-#: taiga/users/models.py:93
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr ""
-#: taiga/users/models.py:96
+#: taiga/users/models.py:134
msgid "active"
msgstr ""
-#: taiga/users/models.py:97
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
-#: taiga/users/models.py:103
+#: taiga/users/models.py:141
msgid "biography"
msgstr ""
-#: taiga/users/models.py:106
+#: taiga/users/models.py:144
msgid "photo"
msgstr ""
-#: taiga/users/models.py:107
+#: taiga/users/models.py:145
msgid "date joined"
msgstr ""
-#: taiga/users/models.py:109
+#: taiga/users/models.py:147
msgid "default language"
msgstr ""
-#: taiga/users/models.py:111
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:113
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr ""
-#: taiga/users/models.py:115
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr ""
-#: taiga/users/models.py:120
+#: taiga/users/models.py:158
msgid "email token"
msgstr ""
-#: taiga/users/models.py:122
+#: taiga/users/models.py:160
msgid "new email address"
msgstr ""
-#: taiga/users/models.py:129
-msgid "max number of private projects owned"
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:132
-msgid "max number of public projects owned"
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:135
+#: taiga/users/models.py:173
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:139
+#: taiga/users/models.py:177
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:256
+#: taiga/users/models.py:294
msgid "permissions"
msgstr ""
@@ -3220,24 +3218,24 @@ msgstr ""
msgid "Invalid username. Try with a different one."
msgstr ""
-#: taiga/users/services.py:52 taiga/users/services.py:69
+#: taiga/users/services.py:53 taiga/users/services.py:70
msgid "Username or password does not matches user."
msgstr ""
-#: taiga/users/services.py:590
+#: taiga/users/services.py:591
msgid "You can't have more private projects"
msgstr ""
-#: taiga/users/services.py:596
+#: taiga/users/services.py:597
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:608
-msgid "You have reached the limit of memberships for private projects"
+#: taiga/users/services.py:609
+msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/users/services.py:614
-msgid "You have reached the limit of memberships for public projects"
+#: taiga/users/services.py:615
+msgid "You have reached your current limit of memberships for public projects"
msgstr ""
#: taiga/users/templates/emails/change_email-body-html.jinja:4
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index 9ce8fbb2..083fb38c 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -11,13 +11,15 @@
# Luis Sebastian Urrutia Fuentes , 2016
# Renelis Abreu Ramirez , 2016
# Taiga Dev Team , 2015-2016
+# Xaviju , 2016
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-29 11:08+0200\n"
-"PO-Revision-Date: 2016-03-28 10:50+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-03-30 12:58+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
"MIME-Version: 1.0\n"
@@ -47,27 +49,27 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Son necesarios. 255 caracteres o menos (letras, números y /./-/_)"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Nombre de usuario no disponible"
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Email no disponible"
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "El token no pertenece a ninguna invitación válida."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Este usuario ya está registrado."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr "Este usuario ya es miembro del proyecto."
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Error al crear un nuevo usuario "
@@ -201,7 +203,7 @@ msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada.
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
#: taiga/hooks/api.py:68 taiga/projects/api.py:629
-#: taiga/projects/issues/api.py:237 taiga/projects/mixins/ordering.py:58
+#: 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
@@ -360,8 +362,8 @@ msgid "Precondition error"
msgstr "Error por incumplimiento de precondición"
#: taiga/base/exceptions.py:217
-msgid "Not enough slots for project."
-msgstr "No hay suficiente espacio para el proyecto."
+msgid "No room left for more projects."
+msgstr ""
#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
@@ -577,21 +579,21 @@ msgstr "error importando las etiquetas"
msgid "error importing timelines"
msgstr "error importando los timelines"
-#: taiga/export_import/serializers.py:177
+#: taiga/export_import/serializers.py:178
msgid "{}=\"{}\" not found in this project"
msgstr "{}=\"{}\" no se ha encontrado en este proyecto"
-#: taiga/export_import/serializers.py:442
+#: taiga/export_import/serializers.py:443
#: taiga/projects/custom_attributes/serializers.py:104
msgid "Invalid content. It must be {\"key\": \"value\",...}"
msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}"
-#: taiga/export_import/serializers.py:457
+#: taiga/export_import/serializers.py:458
#: taiga/projects/custom_attributes/serializers.py:119
msgid "It contain invalid custom fields."
msgstr "Contiene attributos personalizados inválidos."
-#: taiga/export_import/serializers.py:527
+#: taiga/export_import/serializers.py:528
#: taiga/projects/mixins/serializers.py:38
msgid "Name duplicated for the project"
msgstr "Nombre duplicado para el proyecto"
@@ -622,8 +624,8 @@ msgstr ""
"\n"
"Volca de datos de proyecto generado
\n"
"Hola %(user)s,
\n"
-"El volcado de datos de tu proyecto %(project)s se ha generado con "
-"éxisito.
\n"
+"El volcado de datos de tu proyecto %(project)s se ha generado con éxito."
+"
\n"
"Puedes descargarlo aquí:
\n"
"Descargar el archivo con el volcado de datos\n"
@@ -649,7 +651,7 @@ msgstr ""
"\n"
"Hola %(user)s,\n"
"\n"
-"El volcado de datos de tu proyecto %(project)s se ha generado con éxisito. "
+"El volcado de datos de tu proyecto %(project)s se ha generado con éxito. "
"Puedes descargarlo aquí:\n"
"\n"
"%(url)s\n"
@@ -851,7 +853,7 @@ msgstr "Se requiere autenticación"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nombre"
@@ -889,11 +891,11 @@ msgstr "usuario"
msgid "application"
msgstr "aplicación"
-#: taiga/feedback/models.py:24 taiga/users/models.py:100
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr "nombre completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:95
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "dirección de email"
@@ -976,7 +978,7 @@ msgstr ""
msgid "The payload is not a valid json"
msgstr "El payload no es un json válido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:143
+#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El proyecto no existe"
@@ -1341,8 +1343,8 @@ msgid "The user doesn't exist"
msgstr "El usuario no existe"
#: taiga/projects/api.py:366
-msgid "The user must be a member of the project"
-msgstr "El usuario debe ser miembro del proyecto"
+msgid "The user must be already a project member"
+msgstr ""
#: taiga/projects/api.py:668
msgid ""
@@ -1382,7 +1384,7 @@ msgstr "Dueño"
#: taiga/projects/notifications/models.py:73
#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302
msgid "project"
msgstr "Proyecto"
@@ -1421,7 +1423,7 @@ msgstr "está desactualizado"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "orden"
@@ -1442,16 +1444,16 @@ msgid "Talky"
msgstr "Talky"
#: taiga/projects/choices.py:32
-msgid "This project was blocked by nonpayment"
+msgid "This project is blocked due to payment failure"
msgstr ""
#: taiga/projects/choices.py:33
-msgid "This project was blocked by staff"
-msgstr "El proyecto fue bloqueado por la administración"
+msgid "This project is blocked by admin staff"
+msgstr ""
#: taiga/projects/choices.py:34
-msgid "This project was blocked because the owner left"
-msgstr "El proyecto fue bloqueado porque el dueño lo abandonó"
+msgid "This project is blocked because the owner left"
+msgstr ""
#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
@@ -1567,7 +1569,7 @@ msgstr "borrado"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "No asignado"
@@ -1628,23 +1630,23 @@ msgstr "nota de bloqueo"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:158
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tienes permisos para asignar un sprint a esta petición."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this status to this issue."
msgstr "No tienes permisos para asignar un estado a esta petición."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tienes permisos para establecer la gravedad de esta petición."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tienes permiso para establecer la prioridad de esta petición."
-#: taiga/projects/issues/api.py:178
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "No tienes permiso para establecer el tipo de esta petición."
@@ -1701,7 +1703,7 @@ msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
msgid "slug"
msgstr "slug"
@@ -1753,7 +1755,7 @@ msgstr "email"
msgid "create at"
msgstr "creado el"
-#: taiga/projects/models.py:82 taiga/users/models.py:117
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
@@ -1921,7 +1923,7 @@ msgstr "archivado"
#: taiga/projects/models.py:482 taiga/projects/models.py:544
#: taiga/projects/models.py:577 taiga/projects/models.py:600
#: taiga/projects/models.py:627 taiga/projects/models.py:658
-#: taiga/users/models.py:102
+#: taiga/users/models.py:140
msgid "color"
msgstr "color"
@@ -2003,13 +2005,13 @@ msgstr "usuarios notificados"
msgid "Watched"
msgstr "Observado"
-#: taiga/projects/notifications/services.py:66
-#: taiga/projects/notifications/services.py:80
+#: taiga/projects/notifications/services.py:64
+#: taiga/projects/notifications/services.py:78
msgid "Notify exists for specified user and project"
msgstr ""
"Ya existe una política de notificación para este usuario en el proyecto."
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Valor inválido para el nivel de notificación"
@@ -2756,14 +2758,12 @@ msgid "Invalid role for the project"
msgstr "Rol inválido para el proyecto"
#: taiga/projects/serializers.py:195
-msgid "Project owner must be admin."
-msgstr "El propietario del proyecto debe ser administrador."
+msgid "The project owner must be admin."
+msgstr ""
#: taiga/projects/serializers.py:198
-msgid "In this project at least one of the users must be an active admin."
+msgid "At least one user must be an active admin for this project."
msgstr ""
-"En este proyecto al menos uno de los usuarios debe ser un administrador "
-"activo."
#: taiga/projects/serializers.py:394
msgid "Default options"
@@ -2801,11 +2801,11 @@ msgstr "Gravedades"
msgid "Roles"
msgstr "Roles"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
-#: taiga/projects/services/stats.py:218
+#: taiga/projects/services/stats.py:216
msgid "Project End"
msgstr "Final de proyecto"
@@ -3030,12 +3030,12 @@ msgstr ""
"%(new_owner_name)s ha aceptado tu proposición y será el nuevo dueño de "
"\"%(project_name)s\".\n"
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:6
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:10
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
@@ -3043,8 +3043,8 @@ msgstr ""
"\n"
"De ahora en adelante, tu rol para este proyecto será de \"admin\".\n"
-#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:15
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:21
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
msgid ""
@@ -3081,7 +3081,7 @@ msgid ""
" "
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
"\n"
" If you want, you can still try to transfer the project ownership to a "
@@ -3089,8 +3089,8 @@ msgid ""
" "
msgstr ""
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
-#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:23
msgid "Request transfer to a different person"
msgstr "Solicitar transferir a una persona diferente"
@@ -3109,12 +3109,10 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
-msgid ""
-"\n"
-"
%(rejecter_name)s says:
\n"
+msgid "%(rejecter_name)s says:"
msgstr ""
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
"\n"
"If you want, you can still try to transfer the project ownership to a "
@@ -3124,7 +3122,7 @@ msgstr ""
"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a "
"una persona diferente.\n"
-#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:17
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
msgid "Request transfer to a different person:"
msgstr "Solicitar transferir a una persona diferente:"
@@ -3236,8 +3234,8 @@ msgid ""
msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
-msgid "Accept or reject the project transfer:"
-msgstr "Aceptar o rechazar la transferencia de proyecto:"
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
#, python-format
@@ -3577,7 +3575,7 @@ msgstr "última modificación por"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Comprueba la API de histórico para obtener el diff exacto"
@@ -3639,11 +3637,11 @@ msgstr ""
msgid "Invalid, are you sure the token is correct?"
msgstr "Inválido, ¿estás seguro de que el token es correcto?"
-#: taiga/users/models.py:58
+#: taiga/users/models.py:96
msgid "superuser status"
msgstr "es superusuario"
-#: taiga/users/models.py:59
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3651,24 +3649,24 @@ msgstr ""
"Otorga todos los permisos a este usuario sin necesidad de hacerlo "
"explicitamente."
-#: taiga/users/models.py:89
+#: taiga/users/models.py:127
msgid "username"
msgstr "nombre de usuario"
-#: taiga/users/models.py:90
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_"
-#: taiga/users/models.py:93
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Introduce un nombre de usuario válido"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:134
msgid "active"
msgstr "activo"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3676,59 +3674,59 @@ msgstr ""
"Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un "
"usuario."
-#: taiga/users/models.py:103
+#: taiga/users/models.py:141
msgid "biography"
msgstr "biografía"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "fecha de registro"
-#: taiga/users/models.py:109
+#: taiga/users/models.py:147
msgid "default language"
msgstr "idioma por defecto"
-#: taiga/users/models.py:111
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "tema por defecto"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "zona horaria por defecto"
-#: taiga/users/models.py:115
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "añade color a las etiquetas"
-#: taiga/users/models.py:120
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token de email"
-#: taiga/users/models.py:122
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nueva dirección de email"
-#: taiga/users/models.py:129
-msgid "max number of private projects owned"
-msgstr "máximo de proyectos privados poseídos"
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
-#: taiga/users/models.py:132
-msgid "max number of public projects owned"
-msgstr "máximo de proyectos públicos poseídos"
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
-#: taiga/users/models.py:135
+#: taiga/users/models.py:173
msgid "max number of memberships for each owned private project"
msgstr "máximo de membresías para cada proyecto privado poseído"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:177
msgid "max number of memberships for each owned public project"
msgstr "máximo de membresías para cada proyecto público poseído"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permisos"
@@ -3740,25 +3738,25 @@ msgstr "no válido"
msgid "Invalid username. Try with a different one."
msgstr "Nombre de usuario inválido. Prueba con otro."
-#: taiga/users/services.py:52 taiga/users/services.py:69
+#: taiga/users/services.py:53 taiga/users/services.py:70
msgid "Username or password does not matches user."
msgstr "Nombre de usuario o contraseña inválidos."
-#: taiga/users/services.py:590
+#: taiga/users/services.py:591
msgid "You can't have more private projects"
msgstr "No puedes tener más proyectos privados"
-#: taiga/users/services.py:596
+#: taiga/users/services.py:597
msgid "You can't have more public projects"
msgstr "No puedes tener más proyectos públicos"
-#: taiga/users/services.py:608
-msgid "You have reached the limit of memberships for private projects"
-msgstr "Has alcanzado el límite de membresías para proyectos privados"
+#: taiga/users/services.py:609
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
-#: taiga/users/services.py:614
-msgid "You have reached the limit of memberships for public projects"
-msgstr "As alcanzado el límite de membresías para proyectos públicos"
+#: taiga/users/services.py:615
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index be8a9137..3f1cbd01 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/LC_MESSAGES/django.po
@@ -9,9 +9,10 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-03-29 11:08+0200\n"
-"PO-Revision-Date: 2016-03-28 10:50+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-03-30 12:58+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández