diff --git a/AUTHORS.rst b/AUTHORS.rst
index 70045203..ec62d162 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -20,12 +20,18 @@ answer newbie questions, and generally made taiga that much better:
- Andrea Stagi
- Andrés Moya
- Andrey Alekseenko
+<<<<<<< HEAD
+=======
+- Brett Profitt
+>>>>>>> master
- Bruno Clermont
- Chris Wilson
- David Burke
- Hector Colina
- Joe Letts
- Julien Palard
+- luyikei
+- Motius GmbH
- Ricky Posner
- Yamila Moreno
-- Brett Profitt
+- Yaser Alraddadi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a570ed3d..f955a7d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,19 @@
# Changelog #
+## 2.0.0 Pulsatilla Patens (2016-04-04)
+
+### 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
+- Customizable max of memberships per owned private and public projects
+
+### Misc
+- Lots of small and not so small bugfixes.
+
+
## 1.10.0 Dryas Octopetala (2016-01-30)
### Features
@@ -10,7 +23,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:
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 3687486b..568be60c 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",
@@ -434,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"
@@ -522,6 +523,12 @@ WEBHOOKS_ENABLED = False
FRONT_SITEMAP_ENABLED = False
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
+MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit
+MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit
from .sr import *
diff --git a/settings/local.py.example b/settings/local.py.example
index 28f5abb9..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
@@ -24,7 +25,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/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/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/mixins.py b/taiga/base/api/mixins.py
index c558f750..1125fcd9 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -53,9 +53,9 @@ from taiga.base import response
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):
"""
Given a model instance, and an optional pk and slug field,
@@ -243,3 +243,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/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/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/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 %}
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/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/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/decorators.py b/taiga/base/decorators.py
index 0ffe8e10..afddb058 100644
--- a/taiga/base/decorators.py
+++ b/taiga/base/decorators.py
@@ -17,7 +17,6 @@
from django_pglocks import advisory_lock
-
def detail_route(methods=['get'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail requests.
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index 3e7c2104..104ba896 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -201,6 +201,28 @@ class NotAuthenticated(NotAuthenticated):
pass
+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 = _("No room left for more projects.")
+
+ 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
@@ -232,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/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/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py
index c0f1a490..53a63a87 100644
--- a/taiga/base/management/commands/test_emails.py
+++ b/taiga/base/management/commands/test_emails.py
@@ -19,7 +19,8 @@ import datetime
from optparse import make_option
-from django.db.models.loading import get_model
+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,13 +157,13 @@ 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:
- model = get_model(*notification_email[0].split("."))
+ model = apps.get_model(*notification_email[0].split("."))
snapshot = {
"subject": "Tests subject",
"ref": 123123,
@@ -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/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/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/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/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/export_import/api.py b/taiga/export_import/api.py
index 19ccd373..1ae5ca4b 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,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
data = request.DATA.copy()
data['owner'] = data.get('owner', request.user.email)
+ is_private = data.get('is_private', False)
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ Project(is_private=is_private, id=None)
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error)
+
# Create Project
project_serialized = service.store_project(data)
@@ -106,11 +115,19 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create memberships
if "memberships" in data:
+ members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ Project(is_private=is_private, id=None),
+ members
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
service.store_memberships(project_serialized.object, data)
try:
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(
@@ -118,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
@@ -202,6 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
try:
dump = json.load(reader(dump))
+ is_private = dump.get("is_private", False)
except Exception:
raise exc.WrongArguments(_("Invalid dump format"))
@@ -209,11 +227,23 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if slug is not None and Project.objects.filter(slug=slug).exists():
del dump['slug']
+ user = request.user
+ dump['owner'] = user.email
+
+ members = len([m for m in dump.get("memberships", []) if m.get("email", None) != dump["owner"]])
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ user,
+ Project(is_private=is_private, id=None),
+ members
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
+
if settings.CELERY_ENABLED:
- task = tasks.load_project_dump.delay(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..b68f3bf9 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -17,7 +17,8 @@
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
from . import service
@@ -89,7 +90,15 @@ def store_tags_colors(project, data):
def dict_to_project(data, owner=None):
if owner:
- data["owner"] = owner
+ data["owner"] = owner.email
+ members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ owner,
+ Project(is_private=data.get("is_private", False), id=None),
+ members
+ )
+ if not enough_slots:
+ raise TaigaImportError(not_enough_slots_error)
project_serialized = service.store_project(data)
@@ -138,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/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index 1b44adbf..367a2401 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -25,6 +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):
@@ -58,7 +59,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/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/export_import/serializers.py b/taiga/export_import/serializers.py
index 74df0cbc..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
@@ -29,10 +30,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
@@ -154,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):
@@ -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/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/taiga/export_import/templates/emails/dump_project-body-html.jinja b/taiga/export_import/templates/emails/dump_project-body-html.jinja
index 86906ded..18b234c0 100644
--- a/taiga/export_import/templates/emails/dump_project-body-html.jinja
+++ b/taiga/export_import/templates/emails/dump_project-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
+ {% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
Project dump generated
Hello {{ user }},
Your dump from project {{ project }} has been correctly generated.
diff --git a/taiga/export_import/templates/emails/dump_project-body-text.jinja b/taiga/export_import/templates/emails/dump_project-body-text.jinja
index 810d373d..6e12008b 100644
--- a/taiga/export_import/templates/emails/dump_project-body-text.jinja
+++ b/taiga/export_import/templates/emails/dump_project-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
+{% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
Hello {{ user }},
Your dump from project {{ project }} has been correctly generated. You can download it here:
diff --git a/taiga/export_import/templates/emails/dump_project-subject.jinja b/taiga/export_import/templates/emails/dump_project-subject.jinja
index 7ad0ef61..f723a922 100644
--- a/taiga/export_import/templates/emails/dump_project-subject.jinja
+++ b/taiga/export_import/templates/emails/dump_project-subject.jinja
@@ -1 +1 @@
-{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %}
+{% trans project=project.name %}[{{ project }}] Your project dump has been generated{% endtrans %}
diff --git a/taiga/export_import/templates/emails/export_error-body-html.jinja b/taiga/export_import/templates/emails/export_error-body-html.jinja
index 12ab5b10..0ae593e9 100644
--- a/taiga/export_import/templates/emails/export_error-body-html.jinja
+++ b/taiga/export_import/templates/emails/export_error-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %}
+ {% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %}
{{ error_message }}
Hello {{ user }},
Your project {{ project }} has not been exported correctly.
diff --git a/taiga/export_import/templates/emails/export_error-body-text.jinja b/taiga/export_import/templates/emails/export_error-body-text.jinja
index e9efcb16..2eab60a5 100644
--- a/taiga/export_import/templates/emails/export_error-body-text.jinja
+++ b/taiga/export_import/templates/emails/export_error-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %}
+{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %}
Hello {{ user }},
{{ error_message }}
diff --git a/taiga/export_import/templates/emails/export_error-subject.jinja b/taiga/export_import/templates/emails/export_error-subject.jinja
index fed22ace..e5d020cf 100644
--- a/taiga/export_import/templates/emails/export_error-subject.jinja
+++ b/taiga/export_import/templates/emails/export_error-subject.jinja
@@ -1 +1 @@
-{% trans error_subject=error_subject, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %}
+{% trans error_subject=error_subject, project=project.name %}[{{ project }}] {{ error_subject }}{% endtrans %}
diff --git a/taiga/export_import/templates/emails/import_error-body-html.jinja b/taiga/export_import/templates/emails/import_error-body-html.jinja
index b1a27a72..52afe388 100644
--- a/taiga/export_import/templates/emails/import_error-body-html.jinja
+++ b/taiga/export_import/templates/emails/import_error-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %}
+ {% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %}
{{ error_message }}
Hello {{ user }},
Your project has not been importer correctly.
diff --git a/taiga/export_import/templates/emails/import_error-body-text.jinja b/taiga/export_import/templates/emails/import_error-body-text.jinja
index 50f1ccb4..7a9d782e 100644
--- a/taiga/export_import/templates/emails/import_error-body-text.jinja
+++ b/taiga/export_import/templates/emails/import_error-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %}
+{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %}
Hello {{ user }},
{{ error_message }}
diff --git a/taiga/export_import/templates/emails/load_dump-body-html.jinja b/taiga/export_import/templates/emails/load_dump-body-html.jinja
index 52d7fd7f..564ccff3 100644
--- a/taiga/export_import/templates/emails/load_dump-body-html.jinja
+++ b/taiga/export_import/templates/emails/load_dump-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %}
+ {% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %}
Project dump imported
Hello {{ user }},
Your project dump has been correctly imported.
diff --git a/taiga/export_import/templates/emails/load_dump-body-text.jinja b/taiga/export_import/templates/emails/load_dump-body-text.jinja
index e5f0aa24..3cb40ad2 100644
--- a/taiga/export_import/templates/emails/load_dump-body-text.jinja
+++ b/taiga/export_import/templates/emails/load_dump-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %}
+{% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %}
Hello {{ user }},
Your project dump has been correctly imported.
diff --git a/taiga/export_import/templates/emails/load_dump-subject.jinja b/taiga/export_import/templates/emails/load_dump-subject.jinja
index a258d42e..6ef621c4 100644
--- a/taiga/export_import/templates/emails/load_dump-subject.jinja
+++ b/taiga/export_import/templates/emails/load_dump-subject.jinja
@@ -1 +1 @@
-{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %}
+{% trans project=project.name %}[{{ project }}] Your project dump has been imported{% endtrans %}
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/feedback/templates/emails/feedback_notification-body-html.jinja b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja
index b2c38449..3417e7c2 100644
--- a/taiga/feedback/templates/emails/feedback_notification-body-html.jinja
+++ b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
+ {% trans full_name=feedback_entry.full_name, email=feedback_entry.email %}
Feedback
Taiga has received feedback from {{ full_name }} <{{ email }}>
{% endtrans %}
diff --git a/taiga/feedback/templates/emails/feedback_notification-body-text.jinja b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja
index 414501ae..ec2faa8b 100644
--- a/taiga/feedback/templates/emails/feedback_notification-body-text.jinja
+++ b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email, comment=feedback_entry.comment %}---------
+{% trans full_name=feedback_entry.full_name, email=feedback_entry.email, comment=feedback_entry.comment %}---------
- From: {{ full_name }} <{{ email }}>
---------
- Comment:
diff --git a/taiga/feedback/templates/emails/feedback_notification-subject.jinja b/taiga/feedback/templates/emails/feedback_notification-subject.jinja
index e93fdbd1..a5fe9c3f 100644
--- a/taiga/feedback/templates/emails/feedback_notification-subject.jinja
+++ b/taiga/feedback/templates/emails/feedback_notification-subject.jinja
@@ -1,3 +1,3 @@
-{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
+{% trans full_name=feedback_entry.full_name, email=feedback_entry.email %}
[Taiga] Feedback from {{ full_name }} <{{ email }}>
{% endtrans %}
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/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/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)
diff --git a/taiga/front/urls.py b/taiga/front/urls.py
index 76709327..e7ea3e0f 100644
--- a/taiga/front/urls.py
+++ b/taiga/front/urls.py
@@ -46,6 +46,7 @@ urls = {
"team": "/project/{0}/team/", # project.slug
- "project-admin": "/project/{0}/admin/project-profile/details", # project.slug
-}
+ "project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token
+ "project-admin": "/login?next=/project/{0}/admin/project-profile/details", # project.slug
+}
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/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py
index a4a8c00d..30d92c39 100644
--- a/taiga/hooks/bitbucket/event_hooks.py
+++ b/taiga/hooks/bitbucket/event_hooks.py
@@ -40,19 +40,16 @@ class PushEventHook(BaseEventHook):
changes = self.payload.get("push", {}).get('changes', [])
for change in filter(None, changes):
- new = change.get("new", None)
- if not new:
+ commits = change.get("commits", [])
+ if not commits:
continue
- target = new.get("target", None)
- if not target:
- continue
+ for commit in commits:
+ message = commit.get("message", None)
+ if not message:
+ continue
- message = target.get("message", None)
- if not message:
- continue
-
- self._process_message(message, None)
+ self._process_message(message, None)
def _process_message(self, message, bitbucket_user):
"""
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/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 27f087a7..ed198ff2 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
"MIME-Version: 1.0\n"
@@ -41,32 +42,33 @@ 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."
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Token invàlid"
@@ -182,6 +184,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +250,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 +341,16 @@ msgstr "Error d'integritat per argument invàlid o erroni."
msgid "Precondition error"
msgstr "Precondició errònia."
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr ""
@@ -454,71 +469,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 +552,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,12 +718,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nom"
@@ -722,11 +735,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Descripció"
@@ -740,7 +753,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 +761,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: 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:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "Adreça d'email"
@@ -760,11 +773,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: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
#: taiga/userstorage/models.py:28
@@ -836,8 +849,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El projecte no existeix"
@@ -845,26 +858,26 @@ msgstr "El projecte no existeix"
msgid "Bad signature"
msgstr "Firma no vàlida."
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "L'element referenciat no existeix"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "L'estatus no existeix."
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Informació d'incidència no vàlida."
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -875,17 +888,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Informació del comentari a l'incidència no vàlid."
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -896,7 +909,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1135,96 +1148,110 @@ 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:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Format d'image invàlid"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "Ordre"
@@ -1244,18 +1271,34 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1354,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"
@@ -1415,23 +1458,23 @@ msgstr "nota de bloqueig"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: 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"
@@ -1483,29 +1526,29 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1530,224 +1573,232 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "text extra d'invitació"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "Points per defecte"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "estatus d'història d'usuai per defecte"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "Estatus de tasca per defecte"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "Prioritat per defecte"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "Severitat per defecte"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "Status d'incidència per defecte"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "Tipus d'incidència per defecte"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de fites"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
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:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "template de creació"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permisos d'anònims"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permisos d'usuaris"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "es privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colors de tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1814,29 @@ 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 ""
-#: 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 ""
@@ -2287,54 +2338,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Aquest e-mail ja està en ús"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rol invàlid per al projecte"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rols"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -2342,15 +2402,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 +2567,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2728,15 +3029,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 +3096,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 ""
@@ -2811,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 ""
@@ -2819,65 +3120,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:96
msgid "superuser status"
msgstr "estatus de superusuari"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2885,24 +3186,24 @@ msgstr ""
"Designa que aquest usuari te tots els permisos sense asignarli-los "
"explícitament."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "mot d'usuari"
-#: taiga/users/models.py:107
+#: 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:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Introdueix un nom d'usuari vàlid"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "actiu"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -2910,43 +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:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "data d'unió"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "llenguatge per defecte"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "zona horaria per defecte"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "coloritza tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token de correu"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nova adreça de correu"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permissos"
@@ -2958,10 +3275,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 19f5adfd..2fde1a47 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
"MIME-Version: 1.0\n"
@@ -51,32 +52,33 @@ 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."
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Ungültiges Token"
@@ -212,6 +214,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +280,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 +373,16 @@ msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente"
msgid "Precondition error"
msgstr "Voraussetzungsfehler"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
@@ -509,71 +524,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 +607,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,12 +866,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "Name"
@@ -870,11 +883,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Beschreibung"
@@ -888,7 +901,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 +909,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:138
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:133
msgid "email address"
msgstr "E-Mail Adresse"
@@ -908,11 +921,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: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
#: taiga/userstorage/models.py:28
@@ -984,8 +997,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Das Projekt existiert nicht"
@@ -993,26 +1006,26 @@ msgstr "Das Projekt existiert nicht"
msgid "Bad signature"
msgstr "Falsche Signatur"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Das referenzierte Element existiert nicht"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Der Status existiert nicht"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Der Status des BitBucket Commits hat sich geändert"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Ungültige Ticket-Information"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1029,17 +1042,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Ticket erstellt von BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Ungültige Ticket-Kommentar Information"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1056,7 +1069,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1325,96 +1338,110 @@ msgstr "Administrator Projekt Werte"
msgid "Admin roles"
msgstr "Administrator-Rollen"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ungültiges Bildformat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Unglültiger Templatename"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "Reihenfolge"
@@ -1434,18 +1461,34 @@ msgstr "Kunde"
msgid "Talky"
msgstr "Gesprächig"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1544,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"
@@ -1605,27 +1648,27 @@ msgstr "Blockierungsgrund"
msgid "sprint"
msgstr "Sprint"
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: 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."
@@ -1679,27 +1722,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1724,224 +1767,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:78
msgid "email"
msgstr "E-Mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "erstellt am "
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "Token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Einladung Zusatztext "
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "Benutzerreihenfolge"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "voreingestellte Punkte"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "voreingesteller User-Story Status "
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "voreingestellter Aufgabenstatus"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "voreingestellte Priorität "
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "voreingestellte Gewichtung "
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "voreingestellter Ticket Status"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "voreingestellter Ticket Typ"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "Mitglieder"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "Meilensteine Gesamt"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "Story Punkte insgesamt"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "Videokonferenzsystem"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "Zusatzdaten Videokonferenz"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "Vorlage erstellen"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "Rechte für anonyme Nutzer"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "Rechte für registrierte Nutzer"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "ist privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "Tag Farben"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "Count"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +2008,29 @@ 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"
-#: 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"
@@ -2759,56 +2810,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Die E-Mailadresse ist bereits vergeben"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ungültige Rolle für dieses Projekt"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rollen"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
@@ -2816,18 +2874,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 +3075,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3249,19 +3548,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 +3619,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"
@@ -3336,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"
@@ -3344,66 +3643,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:96
msgid "superuser status"
msgstr "Superuser Status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3411,25 +3710,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:127
msgid "username"
msgstr "Benutzername"
-#: taiga/users/models.py:107
+#: 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:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Geben Sie einen gültigen Benuzternamen ein."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3437,43 +3736,59 @@ msgstr ""
"Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen "
"Benutzer zu löschen."
-#: taiga/users/models.py:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr "Über mich"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "Foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "Beitrittsdatum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "Vorgegebene Sprache"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "Standard-Theme"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "Vorgegebene Zeitzone"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "Tag-Farben"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "E-Mail Token"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "neue E-Mail Adresse"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "Berechtigungen"
@@ -3485,10 +3800,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 7e445d3f..6d00fcbd 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-04-01 11:09+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -37,32 +37,33 @@ 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 ""
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr ""
@@ -174,6 +175,15 @@ msgid ""
"corrupted image."
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:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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,16 @@ msgstr ""
msgid "Precondition error"
msgstr ""
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr ""
@@ -443,71 +457,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 +540,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,12 +706,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr ""
@@ -711,11 +723,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr ""
@@ -729,7 +741,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 +749,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr ""
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr ""
@@ -749,11 +761,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: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
#: taiga/userstorage/models.py:28
@@ -809,8 +821,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr ""
@@ -818,26 +830,26 @@ msgstr ""
msgid "Bad signature"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -848,17 +860,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -869,7 +881,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1108,96 +1120,110 @@ msgstr ""
msgid "Admin roles"
msgstr ""
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr ""
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr ""
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 ""
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr ""
@@ -1217,18 +1243,34 @@ msgstr ""
msgid "Talky"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1327,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 ""
@@ -1388,23 +1430,23 @@ msgstr ""
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr ""
@@ -1458,27 +1500,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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 ""
@@ -1503,224 +1545,232 @@ msgstr ""
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:78
msgid "email"
msgstr ""
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr ""
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr ""
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr ""
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr ""
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr ""
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr ""
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr ""
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr ""
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr ""
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr ""
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr ""
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr ""
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr ""
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr ""
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr ""
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr ""
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr ""
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr ""
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr ""
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr ""
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr ""
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr ""
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr ""
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1786,29 @@ 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 ""
-#: 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 ""
@@ -2254,54 +2304,63 @@ 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:172
msgid "Email address is already taken"
msgstr ""
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr ""
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr ""
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -2309,15 +2368,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 +2515,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2677,15 +2977,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 +3044,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 ""
@@ -2760,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 ""
@@ -2768,129 +3068,145 @@ 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:96
msgid "superuser status"
msgstr ""
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr ""
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
-#: taiga/users/models.py:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr ""
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr ""
-#: taiga/users/models.py:114
+#: 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:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr ""
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr ""
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr ""
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr ""
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr ""
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr ""
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr ""
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr ""
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr ""
@@ -2902,10 +3218,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index f3e064b4..f337481a 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -3,19 +3,23 @@
# 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
# Jesus Marin , 2015
+# 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-23 21:41+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
"MIME-Version: 1.0\n"
@@ -45,32 +49,33 @@ 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 "
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Token inválido"
@@ -196,6 +201,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr "Elemento bloqueado"
+
#: 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 +268,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 +361,16 @@ 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/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
@@ -493,71 +511,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 +594,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"
@@ -608,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"
@@ -635,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"
@@ -832,12 +848,12 @@ msgstr "Se requiere autenticación"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py: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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nombre"
@@ -849,11 +865,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descripción"
@@ -867,7 +883,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 +891,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:138
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:133
msgid "email address"
msgstr "dirección de email"
@@ -887,11 +903,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: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
#: taiga/userstorage/models.py:28
@@ -962,8 +978,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "El proyecto no existe"
@@ -971,26 +987,26 @@ msgstr "El proyecto no existe"
msgid "Bad signature"
msgstr "Firma errónea"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "El elemento referenciado no existe"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "El estado no existe"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Estado cambiado desde un commit de BitBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Información inválida de Issue"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1007,17 +1023,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Petición creada desde BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Información de comentario de Issue inválida"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1034,7 +1050,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1302,96 +1318,112 @@ 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:165 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:169 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:230
msgid "Not valid template name"
msgstr "Nombre de plantilla invalido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr "id de usuario inválido"
-#: taiga/projects/api.py:577
+#: 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 already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "orden"
@@ -1411,18 +1443,34 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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 "Url"
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1521,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"
@@ -1582,23 +1630,23 @@ msgstr "nota de bloqueo"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: 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."
@@ -1652,27 +1700,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1699,224 +1747,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:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "creado el"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "texto extra de la invitación"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "orden del usuario"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "puntos por defecto"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "estado de historia por defecto"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "estado de tarea por defecto"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "prioridad por defecto"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "gravedad por defecto"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "estado de petición por defecto"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipo de petición por defecto"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "miembros"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de sprints"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "puntos de historia totales"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "datos extra de videoconferencia"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "creación de plantilla"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permisos de anónimo"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permisos de usuario"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "privado"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "es destacado"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "está buscando a gente"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr "nota (buscando a gente)"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colores de etiquetas"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr "token de transferencia de proyecto"
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "recuento"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans la última semana"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans el último mes"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans el último año"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "actividad la última semana"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "actividad el último mes"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:466
+#: 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:140
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,30 +1988,30 @@ 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"
-#: 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"
@@ -2687,55 +2743,65 @@ 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"
+"No puedes abandonar el proyecto si eres el dueño o no existen más "
+"administradores"
-#: taiga/projects/serializers.py:237
+#: 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:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rol inválido para el proyecto"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roles"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -2743,15 +2809,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 "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."
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 +2994,256 @@ 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
+#, 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\"."
+"p>\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 ""
+"\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:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+"\n"
+"De ahora en adelante, tu rol para este proyecto será de \"admin\".\n"
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+"\n"
+"El Equipo de Taiga\n"
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+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
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr "Solicitar transferir a una persona diferente"
+
+#: 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 ""
+"\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:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+"\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:15
+msgid "Request transfer to a different person:"
+msgstr "Solicitar transferir a una persona diferente:"
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+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
+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 "Continuar"
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3163,17 +3490,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 +3559,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"
@@ -3248,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"
@@ -3256,65 +3583,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 "Restricciones"
+
+#: 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:96
msgid "superuser status"
msgstr "es superusuario"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3322,24 +3649,24 @@ msgstr ""
"Otorga todos los permisos a este usuario sin necesidad de hacerlo "
"explicitamente."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "nombre de usuario"
-#: taiga/users/models.py:107
+#: 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:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Introduce un nombre de usuario válido"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "activo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3347,43 +3674,59 @@ 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:141
msgid "biography"
msgstr "biografía"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "fecha de registro"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "idioma por defecto"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "tema por defecto"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "zona horaria por defecto"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "añade color a las etiquetas"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token de email"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nueva dirección de email"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: 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: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:294
msgid "permissions"
msgstr "permisos"
@@ -3395,10 +3738,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr "No puedes tener más proyectos privados"
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr "No puedes tener más proyectos públicos"
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 525eb7d7..b8031f95 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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
"MIME-Version: 1.0\n"
@@ -42,32 +43,33 @@ msgid ""
msgstr ""
"Vaaditaan. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Käyttäjänimi on varattu."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Sähköposti on jo varattu."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Käyttäjä on jo rekisteröitynyt."
-#: 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 "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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Väärä tunniste"
@@ -184,6 +186,15 @@ msgstr ""
"Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli "
"vioittunut."
+#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +252,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 +343,16 @@ msgstr "Integrity Error for wrong or invalid arguments"
msgid "Precondition error"
msgstr "Precondition error"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
@@ -479,71 +494,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 +577,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,12 +828,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nimi"
@@ -832,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "kuvaus"
@@ -850,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 ""
@@ -858,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:138
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:133
msgid "email address"
msgstr "sähköpostiosoite"
@@ -870,11 +883,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: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
#: taiga/userstorage/models.py:28
@@ -947,8 +960,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projektia ei löydy"
@@ -956,26 +969,26 @@ msgstr "Projektia ei löydy"
msgid "Bad signature"
msgstr "Virheellinen allekirjoitus"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Viitattu elementtiä ei löydy"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Tilaa ei löydy"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Tila muutettu BitBucket kommitilla"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Virheellinen pyynnön tieto"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -986,17 +999,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Virheellinen pyynnön kommentin tieto"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1007,7 +1020,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1264,96 +1277,110 @@ msgstr "Hallinnoi projektin arvoja"
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Väärä kuvaformaatti"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Virheellinen mallipohjan nimi"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "order"
@@ -1373,18 +1400,34 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1483,7 +1526,7 @@ msgstr "poistettu"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Tekijä puuttuu"
@@ -1544,23 +1587,23 @@ msgstr "suljettu muistiinpano"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
msgid "You don't have permissions to set this type to this issue."
msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö."
@@ -1614,27 +1657,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1659,224 +1702,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:78
msgid "email"
msgstr "sähköposti"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "luo täällä"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "tunniste"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "kutsun lisäteksti"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "käyttäjäjärjestys"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "oletuspisteet"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "oletus Kt tila"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "oletus tehtävän tila"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "oletus kiireellisyys"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "oletus vakavuus"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "oletus pyynnön tila"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "oletus pyyntö tyyppi"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "jäsenet"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "virstapyväitä yhteensä"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "käyttäjätarinan yhteispisteet"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "luo mallipohja"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "vieraan oikeudet"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "käyttäjän oikeudet"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "on yksityinen"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "avainsanojen värit"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1943,29 @@ 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 ""
-#: 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 "Ilmoita olemassaolosta määritellyille käyttäjille ja projektille"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr ""
@@ -2656,54 +2707,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Sähköpostiosoite on jo käytössä"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Virheellinen rooli projektille"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roolit"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Tuleva kierros"
@@ -2711,15 +2771,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 +2958,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3128,15 +3429,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 +3496,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"
@@ -3211,7 +3512,7 @@ msgstr "viimeksi muokannut"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -3219,135 +3520,151 @@ 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:96
msgid "superuser status"
msgstr "pääkäyttäjän status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
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:127
msgid "username"
msgstr "käyttäjänimi"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Anna olemassa oleva käyttäjänimi."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktiivinen"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
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:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "kuva"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "liittymispvm"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "oletuskieli"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "oletus aikavyöhyke"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "väritä avainsanat"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "sähköpostitunniste"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "uusi sähköpostiosoite"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "oikeudet"
@@ -3359,10 +3676,26 @@ msgstr "virheellinen"
msgid "Invalid username. Try with a different one."
msgstr "Tuntematon käyttäjänimi, yritä uudelleen."
-#: 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 "Käyttäjätunnus tai salasana eivät ole oikein."
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index d5717f2b..c1a5dbd5 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -15,15 +15,17 @@
# 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-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-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
"MIME-Version: 1.0\n"
@@ -54,32 +56,33 @@ msgid ""
msgstr ""
"Requis. 255 caractères ou moins. Lettres, chiffres et caractères /./-/_'"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Ce nom d'utilisateur est déjà utilisé."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Cette adresse email est déjà utilisée."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Le jeton ne correspond à aucune invitation."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Cet utilisateur est déjà inscrit."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr "L'utilisateur est déjà un membre du projet"
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Erreur à la création du nouvel utilisateur."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Jeton invalide"
@@ -207,6 +210,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr "Élément bloqué"
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
@@ -266,25 +278,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 +371,16 @@ msgstr "Erreur d'intégrité ou arguments invalides"
msgid "Precondition error"
msgstr "Erreur de précondition"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
@@ -512,72 +528,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 +612,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"
@@ -802,6 +816,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
@@ -831,12 +852,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nom"
@@ -848,11 +869,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "description"
@@ -863,10 +884,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 +895,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:138
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:133
msgid "email address"
msgstr "Adresse email"
@@ -886,11 +907,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: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
#: taiga/userstorage/models.py:28
@@ -961,8 +982,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Le projet n'existe pas"
@@ -970,26 +991,26 @@ msgstr "Le projet n'existe pas"
msgid "Bad signature"
msgstr "Signature non valide"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "L'élément référencé n'existe pas"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "L'état n'existe pas"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Statut changé depuis un commit BitBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Information incorrecte sur le problème"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1000,17 +1021,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Ticket créé depuis BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Ignoré"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1021,7 +1042,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1107,6 +1128,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,96 +1290,110 @@ 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:165 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:169 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:230
msgid "Not valid template name"
msgstr "Nom de modèle non valide"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr "Identifiant utilisateur invalide"
-#: taiga/projects/api.py:577
+#: 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 already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "ordre"
@@ -1375,18 +1413,34 @@ msgstr "Personnalisé"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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 "Url"
+
#: taiga/projects/custom_attributes/models.py:39
#: taiga/projects/issues/models.py:47
msgid "type"
@@ -1485,7 +1539,7 @@ msgstr "supprimé"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Non assigné"
@@ -1546,23 +1600,23 @@ msgstr "note bloquée"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
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,27 +1670,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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é"
@@ -1661,224 +1715,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:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "Créé le"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "jeton"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Text supplémentaire de l'invitation"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "classement utilisateur"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "Points par défaut"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "statut de l'HU par défaut"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "Etat par défaut des tâches"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "Priorité par défaut"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "Sévérité par défaut"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "statut du problème par défaut"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "type de problème par défaut"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
-msgstr ""
+msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total des jalons"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "total des points d'histoire"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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: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:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "Modèle de création"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "Permissions anonymes"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "Permission de l'utilisateur"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "est privé"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "est mis en avant"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "est à la recherche de main d'oeuvre"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "couleurs des tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "total"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans la semaine dernière"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans le mois dernier"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans l'année dernière"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "activité de la semaine écoulée"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "activité du mois écoulé"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1956,29 @@ 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"
-#: 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 "La notification existe pour l'utilisateur et le projet spécifiés"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Valeur non valide pour le niveau de notification"
@@ -2282,6 +2344,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
@@ -2429,7 +2493,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,55 +2504,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Adresse email déjà existante"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Rôle non valide pour le projet"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rôles"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futurs"
@@ -2496,17 +2568,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 +2740,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2901,15 +3214,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 +3283,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"
@@ -2984,7 +3299,7 @@ msgstr "dernier modificateur"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -2992,66 +3307,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:96
msgid "superuser status"
msgstr "statut superutilisateur"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3059,25 +3374,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:127
msgid "username"
msgstr "nom d'utilisateur"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Entrez un nom d'utilisateur valide"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "actif"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3085,43 +3400,59 @@ 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:141
msgid "biography"
msgstr "biographie"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "photo"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "date d'inscription"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "langage par défaut"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "thème par défaut"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "Fuseau horaire par défaut"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "changer la couleur des tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "jeton email"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nouvelle adresse email"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permissions"
@@ -3133,10 +3464,26 @@ msgstr "invalide"
msgid "Invalid username. Try with a different one."
msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom."
-#: 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 "Aucun utilisateur avec ce nom ou ce mot de passe."
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index c41f6400..2f042498 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,9 +14,10 @@ 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-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
"MIME-Version: 1.0\n"
@@ -48,32 +49,33 @@ msgstr ""
"Sono richiesti 255 caratteri, o meno, contenenti: lettere, numeri e "
"caratteri /./-/_ "
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Il nome utente appena scelto è già utilizzato."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "L'email inserita è già utilizzata."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Il token non corrisponde a nessun invito valido"
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "L'Utente è già registrato."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr "Questo utente fa già parte del progetto."
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Token non valido"
@@ -193,6 +195,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +261,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 +355,16 @@ 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/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
@@ -503,72 +518,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 +602,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,12 +915,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nome"
@@ -919,11 +932,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrizione"
@@ -937,7 +950,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 +958,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:138
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:133
msgid "email address"
msgstr "Inserisci un indirizzo e-mail valido."
@@ -957,11 +970,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: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
#: taiga/userstorage/models.py:28
@@ -1038,8 +1051,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Il progetto non esiste"
@@ -1047,26 +1060,26 @@ msgstr "Il progetto non esiste"
msgid "Bad signature"
msgstr "Firma non valida"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "L'elemento di riferimento non esiste"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Lo stato non esiste"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Lo stato è stato modificato a seguito di un commit di BitBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Informazione sul problema non valida"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1086,17 +1099,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Problema creato da BItBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Commento sul problema non valido"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1116,7 +1129,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1398,96 +1411,110 @@ 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:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato dell'immagine non valido"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Il nome del template non è valido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "ordine"
@@ -1507,18 +1534,34 @@ msgstr "Personalizzato"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1617,7 +1660,7 @@ msgstr "rimosso"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Non assegnato"
@@ -1678,23 +1721,23 @@ msgstr "nota bloccata"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
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,27 +1791,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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à"
@@ -1794,224 +1837,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:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "creato a "
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "testo ulteriore per l'invito"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "ordine dell'utente"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "punti predefiniti"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "stati predefiniti per le storie utente"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "stati predefiniti del compito"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "priorità predefinita"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "criticità predefinita"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "stato predefinito del problema"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipologia predefinita del problema"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membri"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "tappe totali"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "punti totali della storia"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "ulteriori dati di videoconferenza "
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "creazione del template"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permessi anonimi"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permessi dell'utente"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "è privato"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "in vetrina"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "sta cercando persone"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr "note sulla ricerca delle persone "
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "colori dei tag"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "conta"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "fans nella settimana"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "fans nel mese"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "fans nell'anno"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "attività nella settimana"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "attività nel mese"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +2078,29 @@ 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"
-#: 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 "La notifica esiste per l'utente e il progetto specificati"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Valore non valido per il livello di notifica"
@@ -2931,54 +2982,63 @@ 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:172
msgid "Email address is already taken"
msgstr "L'indirizzo email è già usato"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ruolo di progetto non valido"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Ruoli"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuri"
@@ -2986,16 +3046,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 +3253,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3425,16 +3726,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 +3794,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"
@@ -3509,7 +3810,7 @@ msgstr "ultima modificatore"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Controlla le API della storie per la differenza esatta"
@@ -3517,66 +3818,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:96
msgid "superuser status"
msgstr "Stato del super-utente"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3584,26 +3885,26 @@ msgstr ""
"Definisce che questo utente ha tutti i permessi senza assegnarglieli "
"esplicitamente."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "nome utente"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Inserisci un nome utente valido."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "attivo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3611,43 +3912,59 @@ 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:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "fotografia"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "data di inizio partecipazione"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "lingua predefinita"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "tema predefinito"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "timezone predefinita"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "colora i tag"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nuovo indirizzo e-mail"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permessi"
@@ -3659,10 +3976,26 @@ msgstr "non valido"
msgid "Invalid username. Try with a different one."
msgstr "Nome utente non valido. Provane uno diverso."
-#: 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 "Il nome utente o la password non corrispondono all'utente."
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index 7e499bcb..c6cc1c9e 100644
--- a/taiga/locale/nl/LC_MESSAGES/django.po
+++ b/taiga/locale/nl/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-01-29 10:21+0100\n"
-"PO-Revision-Date: 2016-01-22 12:10+0000\n"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
"MIME-Version: 1.0\n"
@@ -41,32 +42,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Verplicht. 255 tekens of minder. Letters, nummers en /./-/_ tekens'"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Gebruikersnaame is al in gebruik."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "E-mail adres is al in gebruik."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Token stemt niet overeen met een geldige uitnodiging."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Gebruiker is al geregistreerd."
-#: 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 "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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Ongeldig token"
@@ -193,6 +195,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +261,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 +353,16 @@ msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten"
msgid "Precondition error"
msgstr "Preconditie fout"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
@@ -492,71 +507,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 +590,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,12 +778,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "naam"
@@ -782,11 +795,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "omschrijving"
@@ -800,7 +813,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 +821,11 @@ msgstr ""
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
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:133
msgid "email address"
msgstr "e-mail adres"
@@ -820,11 +833,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: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
#: taiga/userstorage/models.py:28
@@ -896,8 +909,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Het project bestaat niet"
@@ -905,26 +918,26 @@ msgstr "Het project bestaat niet"
msgid "Bad signature"
msgstr "Slechte signature"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Het element waarnaar verwezen wordt bestaat niet"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "De status bestaat niet"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Status veranderd door Bitbucket commit"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Ongeldige issue informatie"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -935,17 +948,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Ongeldige issue commentaar informatie"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -956,7 +969,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1198,96 +1211,110 @@ msgstr "Admin project waarden"
msgid "Admin roles"
msgstr "Admin rollen"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Ongeldig afbeelding formaat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Ongeldige template naam"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "volgorde"
@@ -1307,18 +1334,34 @@ msgstr ""
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
-msgid "Text"
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1417,7 +1460,7 @@ msgstr "verwijderd"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Niet toegewezen"
@@ -1478,25 +1521,25 @@ msgstr "geblokkeerde notitie"
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
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,27 +1593,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1595,224 +1638,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:78
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "aangemaakt op"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "uitnodiging extra text"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "gebruiker volgorde"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "standaard punten"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "standaard US status"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "default taak status"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "standaard prioriteit"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "standaard ernstniveau"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "standaard issue status"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "standaard issue type"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "leden"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "totaal van de milestones"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "totaal story points"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "aanmaak template"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonieme toestemmingen"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "gebruikers toestemmingen"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "is privé"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "tag kleuren"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr ""
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1879,29 @@ 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 ""
-#: 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 "Verwittiging bestaat voor gespecifieerde gebruiker en project"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr ""
@@ -2376,54 +2427,63 @@ 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:172
msgid "Email address is already taken"
msgstr "E-mail adres is al in gebruik"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Ongeldige rol voor project"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Rollen"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Toekomstige sprint"
@@ -2431,15 +2491,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 +2651,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2823,15 +3124,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 +3191,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"
@@ -2906,7 +3207,7 @@ msgstr "gebruiker met laatste wijziging"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -2914,64 +3215,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:96
msgid "superuser status"
msgstr "superuser status"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2979,24 +3280,24 @@ msgstr ""
"Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe "
"te wijzen."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "gebruikersnaam"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Geef een geldige gebruikersnaam in"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "actief"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3004,43 +3305,59 @@ 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:141
msgid "biography"
msgstr "biografie"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "toetrededatum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "standaard taal"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "standaard tijdzone"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "kleur tags"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "e-mail token"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nieuw e-mail adres"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "toestemmingen"
@@ -3052,10 +3369,26 @@ msgstr "ongeldig"
msgid "Invalid username. Try with a different one."
msgstr "Ongeldige gebruikersnaam. Probeer met een andere."
-#: 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 "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker."
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 0d5a6368..69cce377 100644
--- a/taiga/locale/pl/LC_MESSAGES/django.po
+++ b/taiga/locale/pl/LC_MESSAGES/django.po
@@ -10,9 +10,10 @@ 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"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
"MIME-Version: 1.0\n"
@@ -43,32 +44,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Wymagane. Maksymalnie 255 znaków. Litery, cyfry oraz /./-/_ "
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Nazwa użytkownika jest już używana."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Ten adres email jest już w użyciu."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Token nie zgadza się z żadnym zaproszeniem"
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Użytkownik już zarejestrowany"
-#: 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 "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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Nieprawidłowy token"
@@ -187,6 +189,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +255,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 +348,16 @@ 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/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
@@ -493,71 +508,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 +591,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,12 +846,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "nazwa"
@@ -850,11 +863,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "opis"
@@ -868,7 +881,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 +889,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:138
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:133
msgid "email address"
msgstr "adres e-mail"
@@ -888,11 +901,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: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
#: taiga/userstorage/models.py:28
@@ -964,8 +977,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projekt nie istnieje"
@@ -973,26 +986,26 @@ msgstr "Projekt nie istnieje"
msgid "Bad signature"
msgstr "Błędna sygnatura"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Element referencyjny nie istnieje"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Status nie istnieje"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Status zmieniony przez commit z BitBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Nieprawidłowa informacja o zgłoszeniu"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1009,17 +1022,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Zgłoszenie utworzone przez BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Nieprawidłowa informacja o komentarzu do zgłoszenia"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1036,7 +1049,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1306,96 +1319,110 @@ 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:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Niepoprawny format obrazka"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nieprawidłowa nazwa szablonu"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "kolejność"
@@ -1415,18 +1442,34 @@ msgstr "Niestandardowy"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1525,7 +1568,7 @@ msgstr "usuniete"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Nieprzypisane"
@@ -1586,23 +1629,23 @@ msgstr "zaglokowana notatka"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
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,27 +1699,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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ść"
@@ -1701,224 +1744,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:78
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "utwórz na"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "dodatkowy tekst w zaproszeniu"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "kolejność użytkowników"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "domyślne punkty"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "domyślny status dla HU"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "domyślny status dla zadania"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "domyślny priorytet"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "domyślna ważność"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "domyślny status dla zgłoszenia"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "domyślny typ dla zgłoszenia"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "członkowie"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "wszystkich kamieni milowych"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "wszystkich punktów "
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "dodatkowe dane dla wideokonferencji"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "szablon "
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "uprawnienia anonimowych"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "uprawnienia użytkownika"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "jest prywatna"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "kolory tagów"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "ilość"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1985,29 @@ 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"
-#: 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 "Powiadomienie istnieje dla określonego użytkownika i projektu"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Nieprawidłowa wartość dla poziomu notyfikacji"
@@ -2713,54 +2764,63 @@ 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:172
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:184
msgid "Invalid role for the project"
msgstr "Nieprawidłowa rola w projekcie"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Role"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Przyszły sprint"
@@ -2768,16 +2828,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 +3017,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3188,17 +3489,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 +3558,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"
@@ -3273,7 +3574,7 @@ msgstr "ostatnio zmodyfikowane przez"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Dla pełengo diffa sprawdź API historii"
@@ -3281,68 +3582,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:96
msgid "superuser status"
msgstr "status SUPERUSER"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3350,24 +3651,24 @@ msgstr ""
"Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności "
"ich przydzielania."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "nazwa użytkownika"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Wprowadź poprawną nazwę użytkownika"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktywny"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3375,43 +3676,59 @@ 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:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "zdjęcie"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "data dołączenia"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "domyślny język Taiga"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "domyślny szablon Taiga"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "domyśla strefa czasowa"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "kolory tagów"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "tokem e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "nowy adres e-mail"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "uprawnienia"
@@ -3423,10 +3740,26 @@ msgstr "Niepoprawne"
msgid "Invalid username. Try with a different one."
msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną."
-#: 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 "Nazwa użytkownika lub hasło są nieprawidłowe"
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 9e3d6598..7c9c5270 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -19,9 +19,10 @@ 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"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
@@ -51,32 +52,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Obrigatório. No máximo 255 caracteres. Letras, números e /./-/_ ."
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Nome de usuário já está em uso."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "Este e-mail já está em uso."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Esse token não bate com nenhum convite."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Este usuário já está registrado."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr "O usuário já é membro do projeto."
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Erro ao criar um novo usuário."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Token inválido"
@@ -194,6 +196,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +262,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 +355,16 @@ 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/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
@@ -501,71 +516,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 +599,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,12 +853,12 @@ msgstr "Autenticação necessária"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py: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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nome"
@@ -857,11 +870,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrição"
@@ -875,7 +888,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 +896,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:138
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:133
msgid "email address"
msgstr "endereço de e-mail"
@@ -895,11 +908,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: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
#: taiga/userstorage/models.py:28
@@ -971,8 +984,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "O projeto não existe"
@@ -980,26 +993,26 @@ msgstr "O projeto não existe"
msgid "Bad signature"
msgstr "Assinatura Ruim"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "O elemento referenciado não existe"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "O estatus não existe"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Status alterado em Bitbucket commit"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Informação de caso inválida"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1016,17 +1029,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Caso criado pelo Bitbucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Informação de comentário de caso inválido"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1043,7 +1056,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1312,96 +1325,110 @@ 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:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Formato de imagem inválida"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Nome de template inválido"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "ordem"
@@ -1421,18 +1448,34 @@ msgstr "Personalizado"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1531,7 +1574,7 @@ msgstr "removido"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Não-atribuído"
@@ -1592,23 +1635,23 @@ msgstr "nota bloqueada"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Você não tem permissão para colocar esse sprint para esse caso."
-#: taiga/projects/issues/api.py:167
+#: taiga/projects/issues/api.py:162
msgid "You don't have permissions to set this status to this issue."
msgstr "Você não tem permissão para colocar esse status para esse caso."
-#: taiga/projects/issues/api.py:171
+#: taiga/projects/issues/api.py:166
msgid "You don't have permissions to set this severity to this issue."
msgstr "Você não tem permissão para colocar essa severidade para esse caso."
-#: taiga/projects/issues/api.py:175
+#: taiga/projects/issues/api.py:170
msgid "You don't have permissions to set this priority to this issue."
msgstr "Você não tem permissão para colocar essa prioridade para esse caso."
-#: taiga/projects/issues/api.py:179
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "Você não tem permissão para colocar esse tipo para esse caso."
@@ -1662,27 +1705,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1707,224 +1750,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:78
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "criado em"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "texto extra de convite"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "ordem de usuário"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "pontos padrão"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "status de US padrão"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "status padrão de tarefa"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "prioridade padrão"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "severidade padrão"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "status padrão de caso"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "tipo padrão de caso"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "membros"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "total de marcos de progresso"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "pontos totais de US"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 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:187
msgid "creation template"
msgstr "template de criação"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "permissão anônima"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "permissão de usuário"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "é privado"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "cores de tags"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "contagem"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1991,29 @@ 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"
-#: 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 "Existe notificação para usuário e projeto especifcado"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Valor inválido para nível de notificação"
@@ -2697,54 +2748,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Endereço de e-mail já utilizado"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Função inválida para projeto"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Funções"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -2752,15 +2812,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 +3000,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3171,15 +3472,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 +3539,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"
@@ -3254,7 +3555,7 @@ msgstr "último modificador"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Verifique o histórico da API para a exata diferença"
@@ -3262,66 +3563,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:96
msgid "superuser status"
msgstr "status de superuser"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3329,24 +3630,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:127
msgid "username"
msgstr "usuário"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Digite um usuário válido"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "ativo"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3354,43 +3655,59 @@ 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:141
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "data ingressado"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "lingua padrão"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "tema padrão"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "fuso horário padrão"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "tags coloridas"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "token de e-mail"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "novo endereço de email"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "permissões"
@@ -3402,10 +3719,26 @@ msgstr "inválido"
msgid "Invalid username. Try with a different one."
msgstr "Usuário inválido. Tente com um diferente."
-#: 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 "Usuário ou senha não correspondem ao usuário"
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index c38ba995..512d232e 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -9,14 +9,16 @@
# 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-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
"MIME-Version: 1.0\n"
@@ -48,32 +50,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Обязательно. 255 символов или меньше. Буквы, числа и символы /./-/_"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
-msgstr "Это имя пользователя уже используется."
+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 "Ошибка при создании нового пользователя."
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Неверный токен"
@@ -196,6 +199,15 @@ 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:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Страница не является 'последней' и не может быть приведена к int."
@@ -253,24 +265,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 +292,7 @@ msgstr "Ошибка соединения."
#: taiga/base/exceptions.py:77
msgid "Malformed request."
-msgstr "Неверное сформированный запрос."
+msgstr "Неверно сформированный запрос."
#: taiga/base/exceptions.py:82
msgid "Incorrect authentication credentials."
@@ -345,12 +357,16 @@ msgstr "Ошибка целостности из-за неправильных
msgid "Precondition error"
msgstr "Ошибка предусловия"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
@@ -502,71 +518,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 +601,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,12 +853,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "имя"
@@ -856,11 +870,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "описание"
@@ -874,7 +888,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 +896,11 @@ msgstr "пользователь"
msgid "application"
msgstr "приложение"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr "полное имя"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "адрес email"
@@ -894,11 +908,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: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
#: taiga/userstorage/models.py:28
@@ -970,8 +984,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Проект не существует"
@@ -979,26 +993,26 @@ msgstr "Проект не существует"
msgid "Bad signature"
msgstr "Плохая подпись"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Указанный элемент не существует"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Статус не существует"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Статус изменён из-за вклада с BitBucket"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Неверная информация о запросе"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1015,17 +1029,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Запрос создан из BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Неправильная информация в комментарии к запросу"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1042,7 +1056,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1312,97 +1326,110 @@ msgstr "Управлять значениями проекта"
msgid "Admin roles"
msgstr "Управлять ролями"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Неправильный формат изображения"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Неверное название шаблона"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
msgstr ""
-"По крайней мере один пользователь должен быть активным администратором."
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "проект"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "порядок"
@@ -1422,18 +1449,34 @@ msgstr "Специальный"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1532,7 +1575,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 "Не назначено"
@@ -1593,27 +1636,27 @@ msgstr "Заметка о блокировке"
msgid "sprint"
msgstr "спринт"
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса"
@@ -1667,27 +1710,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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 "доступность"
@@ -1714,224 +1757,232 @@ msgstr "параметр '{param}' является обязательным"
msgid "'project' parameter is mandatory"
msgstr "параметр 'project' является обязательным"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:78
msgid "email"
msgstr "электронная почта"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "создано"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "идентификатор"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "дополнительный текст к приглашению"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "порядок пользователей"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Этот пользователем уже является участником проекта"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "очки по умолчанию"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "статусы ПИ по умолчанию"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "статус задачи по умолчанию"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "приоритет по умолчанию"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "важность по умолчанию"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "статус запроса по умолчанию"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "тип запроса по умолчанию"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "лготип"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "участники"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "общее количество вех"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "очки истории"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "активная панель списка задач"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:173 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:175 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:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "панель активных запросов"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "система видеоконференций"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "дополнительные данные системы видеоконференций"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "шаблон для создания"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "права анонимов"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "права пользователя"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "личное"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "цвета тэгов"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "количество"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "активность за неделю"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "активность за месяц"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1998,29 @@ 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 "Просмотренные"
-#: 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 "Неверное значение для уровня уведомлений"
@@ -2712,54 +2763,63 @@ 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:172
msgid "Email address is already taken"
msgstr "Этот почтовый адрес уже используется"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Неверная роль для этого проекта"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Роли"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Будущий спринт"
@@ -2767,16 +2827,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 +3018,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3190,17 +3491,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 +3560,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' является обязательным"
@@ -3275,7 +3576,7 @@ msgstr "последний отредактировавший"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Свертесть с историей API для получения изменений"
@@ -3283,129 +3584,145 @@ 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:96
msgid "superuser status"
msgstr "статус суперпользователя"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит."
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "имя пользователя"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "Введите корректное имя пользователя."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "активный"
-#: taiga/users/models.py:114
+#: 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:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr "биография"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "фотография"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "когда присоединился"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "язык по умолчанию"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "тема по умолчанию"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "временная зона по умолчанию"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "установить цвета для тэгов"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "email токен"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "новый email адрес"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "разрешения"
@@ -3417,10 +3734,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index a97be689..49e7041c 100644
--- a/taiga/locale/sv/LC_MESSAGES/django.po
+++ b/taiga/locale/sv/LC_MESSAGES/django.po
@@ -8,9 +8,10 @@ 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"
-"Last-Translator: Taiga Dev Team \n"
+"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
"MIME-Version: 1.0\n"
@@ -40,32 +41,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Kräver färre än 255 tecken. Kan vara tecken, nummer och /./-/_."
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Användarnamnet används redan"
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "E-postadressen används redan"
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Förekomsten passar inte invitationen. "
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Användaren finns redan."
-#: 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 "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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Felaktig förekomst. "
@@ -186,6 +188,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
@@ -244,24 +255,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 +347,16 @@ msgstr "Integritetsfel för felaktiga eller ogiltiga argument"
msgid "Precondition error"
msgstr "Förutsättningsfel"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
@@ -477,71 +492,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 +575,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,12 +741,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "namn"
@@ -745,11 +758,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "beskrivning"
@@ -763,7 +776,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 +784,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:138
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:133
msgid "email address"
msgstr "e-postadress"
@@ -783,11 +796,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: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
#: taiga/userstorage/models.py:28
@@ -843,8 +856,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Projektet existerar inte"
@@ -852,26 +865,26 @@ msgstr "Projektet existerar inte"
msgid "Bad signature"
msgstr "Dålig signatur"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Referenselementet existerar inte"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Statusen existerar inte"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Status ändrad från BitBucket skrivs in"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Felaktig ärendeinformation"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -882,17 +895,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Ärende skapades från BitBucket."
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Felaktigt kommentarinformation för ärendet"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -903,7 +916,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1153,96 +1166,110 @@ msgstr "Administrera projektvärden"
msgid "Admin roles"
msgstr "Administratorroller"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Felaktigt bildformat"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Inget giltigt mallnamn"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "sortera"
@@ -1262,18 +1289,34 @@ msgstr "Anpassa"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1372,7 +1415,7 @@ msgstr "borttaget"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Ej tilldelad"
@@ -1433,23 +1476,23 @@ msgstr "blockerad notering"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
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,27 +1546,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1548,224 +1591,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:78
msgid "email"
msgstr "e-post"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "skapa som"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "textsträng"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Invitation - extra text"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "användarorder"
-#: taiga/projects/models.py:118
+#: 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:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "standardpoäng"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "standard US-poäng"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "standard status för uppgift"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "standard prioritet"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "standard allvarsgrad"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "standard status för ärende"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "standard typ för ärende"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "medlemmar"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "totalt antal milstolpar"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "totalt antal historiepoäng"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "videokonferensssystem"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferens - extra data"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "mall skapas"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonyma rättigheter"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "användarbehörigheter"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "är privat"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "färger för taggar"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: 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:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1832,29 @@ 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"
-#: 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 "Notifiering finns för användaren och projektet"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Felaktigt värde för notifieringen"
@@ -2299,54 +2350,63 @@ 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:172
msgid "Email address is already taken"
msgstr "E-postadressen är redan använd"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Fel roll for projektet"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roller"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Framtidig sprint"
@@ -2354,15 +2414,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 +2561,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2732,18 +3033,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 +3103,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"
@@ -2818,7 +3119,7 @@ msgstr "senastste ändring"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "Kolla historie API för exakt skillnad"
@@ -2826,66 +3127,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:96
msgid "superuser status"
msgstr "status för administratorn"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -2893,25 +3194,25 @@ msgstr ""
"Anger om användaren har alla behörigheter utan att uttryckligen tilldela "
"dem. "
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "användarnamn"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Skriv in ett giltigt användarnamn"
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:114
+#: taiga/users/models.py:135
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -2919,43 +3220,59 @@ 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:141
msgid "biography"
msgstr "biografi"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "blev medlem datum"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "standardspråk"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "standardtema"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "standard tidzon"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "farglägg taggar"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "e-poststräng"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "ny e-postadress"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "behörigheter"
@@ -2967,10 +3284,26 @@ msgstr "felaktigt"
msgid "Invalid username. Try with a different one."
msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn."
-#: 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 "Användarnamn eller lösenord passar inte."
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index b0bfc3c0..d5aa3d2a 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,9 +10,10 @@ 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-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
"MIME-Version: 1.0\n"
@@ -43,32 +44,33 @@ msgid ""
msgstr ""
"Zorunlu. 255 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri"
-#: taiga/auth/services.py:74
+#: taiga/auth/services.py:75
msgid "Username is already in use."
msgstr "Kullanıcı adı zaten kullanımda."
-#: taiga/auth/services.py:77
+#: taiga/auth/services.py:78
msgid "Email is already in use."
msgstr "E-posta zaten kullanımda."
-#: taiga/auth/services.py:93
+#: taiga/auth/services.py:94
msgid "Token not matches any valid invitation."
msgstr "Kupon geçerli hiç bir davetle uyuşmuyor."
-#: taiga/auth/services.py:121
+#: taiga/auth/services.py:122
msgid "User is already registered."
msgstr "Kullanıcı zaten kayıtlı."
-#: taiga/auth/services.py:145
+#: taiga/auth/services.py:146
msgid "This user is already a member of the project."
msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi."
-#: taiga/auth/services.py:171
+#: taiga/auth/services.py:172
msgid "Error on creating new user."
msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35
+#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "Geçersiz kupon"
@@ -194,6 +196,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:209
+#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: 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 +262,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 +353,16 @@ 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/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
@@ -487,71 +502,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 +585,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,12 +836,12 @@ msgstr "Kimlik doğrulama gerekli"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py: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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "isim"
@@ -840,11 +853,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "tanı"
@@ -858,7 +871,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 +879,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:138
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:133
msgid "email address"
msgstr "e-posta adresi"
@@ -878,11 +891,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: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
#: taiga/userstorage/models.py:28
@@ -952,8 +965,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "Proje mevcut değil."
@@ -961,26 +974,26 @@ msgstr "Proje mevcut değil."
msgid "Bad signature"
msgstr "Kötü imza"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "Referans gösterilmiş varlık mevcut değil"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "Durum mevcut değil"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "Bitbucket commiti ile durum değişti"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "Geçersiz talep bilgisi"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -991,17 +1004,17 @@ msgid ""
"{description}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "Bitbucket ten oluşturulan talep"
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "Geçersiz talep yorum bilgisi"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1012,7 +1025,7 @@ msgid ""
"{message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1260,96 +1273,110 @@ 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:165 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:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "Geçersiz resim biçemi"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "Geçersiz şablon adı"
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "sıra"
@@ -1369,18 +1396,34 @@ msgstr "Özel"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1479,7 +1522,7 @@ msgstr "silindi"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57
+#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
msgid "Unassigned"
msgstr "Atanmamış"
@@ -1540,23 +1583,23 @@ msgstr "engellenmiş not"
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:163
+#: taiga/projects/issues/api.py:158
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:162
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:166
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:170
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:174
msgid "You don't have permissions to set this type to this issue."
msgstr "Bu talep için bu tipi ayarlamaya yetkiniz yok."
@@ -1610,27 +1653,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1655,224 +1698,232 @@ msgstr "'{param}' parametresi zorunlu"
msgid "'project' parameter is mandatory"
msgstr "'proje' parametresi zorunlu"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:78
msgid "email"
msgstr "e-posta"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "kupon"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "Davetiye ekstra metni"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "kullanıcı sırası"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "Kullanıcı zaten projenin üyesi"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "varsayılan puanlar"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "varsayılan KH durumu"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "varsayılan görev durumu"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "varsayılan öncelik"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "varsayılan önem derecesi"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "varsayılan talep durumu"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "varsayılan talep tipi"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "üyeler"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "aşamaların toplamı"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "toplam hikaye puanı"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 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:173 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:175 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:177 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:180 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:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "videokonferans ekstra verisi"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "oluşturma şablonu"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "anonim izinler"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "kullanıcı izinleri"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "gizli"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr "vitrinde"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "insan arıyor"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "etiket renkleri"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "sayı"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "geçen hafta fanları"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "geçen ayın fanları"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "geçen yılın fanları"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "geçen haftanın aktiviteleri"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "geçen ayın aktiviteleri"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1939,29 @@ 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"
-#: 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 "Belirtilen kullanıcı ve proje için bilgilendirme mevcut"
-#: taiga/projects/notifications/services.py:429
+#: taiga/projects/notifications/services.py:427
msgid "Invalid value for notify level"
msgstr "Bildirim düzeyi için geçersiz değer"
@@ -2472,54 +2523,63 @@ 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:172
msgid "Email address is already taken"
msgstr "E-posta adresi önceden alınmış"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "Proje için geçersiz rol"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "Roller"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Gelecek sprint"
@@ -2527,15 +2587,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 +2746,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -2907,15 +3208,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 +3275,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"
@@ -2990,7 +3291,7 @@ msgstr "son düzenleyen"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr ""
@@ -2998,132 +3299,148 @@ 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:96
msgid "superuser status"
msgstr "superuser durumu"
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "kullanıcı adı"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
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:131
msgid "Enter a valid username."
msgstr "Geçerli bir kullanıcı adı girin."
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "aktif"
-#: taiga/users/models.py:114
+#: 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:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr "biyografi"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "fotoğraf"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "katılma tarihi"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "varsayılan dil"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "varsayılan tema"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "varsayılan saat dilimi"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "etiketleri renklendir"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "e-posta kuponu"
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "yeni e-posta adresi"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "izinler"
@@ -3135,10 +3452,26 @@ msgstr "Geçersiz"
msgid "Invalid username. Try with a different one."
msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin."
-#: 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 "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor"
+#: taiga/users/services.py:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index dacffc4b..2136848c 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,9 +11,10 @@ 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-04-01 11:09+0200\n"
+"PO-Revision-Date: 2016-03-30 10:59+0000\n"
+"Last-Translator: Alejandro Alonso Fernández \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
"MIME-Version: 1.0\n"
@@ -43,32 +44,33 @@ msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "必填。最多255字元(可為數字,字母,符號....)"
-#: 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 "無法創建新使用者"
#: 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:376
+#: taiga/projects/api.py:397
msgid "Invalid token"
msgstr "無效的代碼 "
@@ -180,6 +182,15 @@ msgid ""
"corrupted image."
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:233 taiga/projects/mixins/ordering.py:58
+#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
+#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
+#: taiga/webhooks/api.py:67
+msgid "Blocked element"
+msgstr ""
+
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "頁數不是最後,或者它無法轉成整數 "
@@ -237,23 +248,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 +339,16 @@ msgstr "因錯誤或無效參數,一致性出錯"
msgid "Precondition error"
msgstr "前提出錯"
-#: taiga/base/filters.py:78
+#: taiga/base/exceptions.py:217
+msgid "No room left for more projects."
+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/projects/filters.py:43
+#: taiga/base/filters.py:133 taiga/base/filters.py:232
+#: taiga/projects/filters.py:59
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
@@ -484,71 +499,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 +582,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,12 +833,12 @@ 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/users/models.py:251 taiga/webhooks/models.py:28
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
+#: taiga/projects/models.py:472 taiga/projects/models.py:511
+#: taiga/projects/models.py:536 taiga/projects/models.py:573
+#: taiga/projects/models.py:596 taiga/projects/models.py:619
+#: taiga/projects/models.py:654 taiga/projects/models.py:677
+#: taiga/users/models.py:289 taiga/webhooks/models.py:28
msgid "name"
msgstr "姓名"
@@ -837,11 +850,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:150
+#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "描述"
@@ -855,7 +868,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 +876,11 @@ msgstr "使用者"
msgid "application"
msgstr "應用程式"
-#: taiga/feedback/models.py:24 taiga/users/models.py:117
+#: taiga/feedback/models.py:24 taiga/users/models.py:138
msgid "full name"
msgstr "全名"
-#: taiga/feedback/models.py:26 taiga/users/models.py:112
+#: taiga/feedback/models.py:26 taiga/users/models.py:133
msgid "email address"
msgstr "電子郵件"
@@ -875,11 +888,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: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
#: taiga/userstorage/models.py:28
@@ -948,8 +961,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:139
+#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
msgid "The project doesn't exist"
msgstr "專案不存在"
@@ -957,26 +970,26 @@ msgstr "專案不存在"
msgid "Bad signature"
msgstr "錯誤簽名"
-#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76
+#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
#: taiga/hooks/gitlab/event_hooks.py:74
msgid "The referenced element doesn't exist"
msgstr "參考元素不存在"
-#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83
+#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
#: taiga/hooks/gitlab/event_hooks.py:81
msgid "The status doesn't exist"
msgstr "狀態不存在"
-#: taiga/hooks/bitbucket/event_hooks.py:98
+#: taiga/hooks/bitbucket/event_hooks.py:95
msgid "Status changed from BitBucket commit"
msgstr "來自BitBucket 投入的狀態更新"
-#: taiga/hooks/bitbucket/event_hooks.py:127
+#: taiga/hooks/bitbucket/event_hooks.py:124
#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
msgid "Invalid issue information"
msgstr "無效的問題資訊"
-#: taiga/hooks/bitbucket/event_hooks.py:143
+#: taiga/hooks/bitbucket/event_hooks.py:140
#, python-brace-format
msgid ""
"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -993,17 +1006,17 @@ msgstr ""
"\n"
"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:154
+#: taiga/hooks/bitbucket/event_hooks.py:151
msgid "Issue created from BitBucket."
msgstr "來自BitBucket的問題:"
-#: taiga/hooks/bitbucket/event_hooks.py:178
+#: taiga/hooks/bitbucket/event_hooks.py:175
#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
#: taiga/hooks/gitlab/event_hooks.py:153
msgid "Invalid issue comment information"
msgstr "無效的議題評論資訊"
-#: taiga/hooks/bitbucket/event_hooks.py:186
+#: taiga/hooks/bitbucket/event_hooks.py:183
#, python-brace-format
msgid ""
"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
@@ -1020,7 +1033,7 @@ msgstr ""
"\n"
"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:197
+#: taiga/hooks/bitbucket/event_hooks.py:194
#, python-brace-format
msgid ""
"Comment From BitBucket:\n"
@@ -1286,96 +1299,110 @@ msgstr "管理員專案數值"
msgid "Admin roles"
msgstr "管理員角色"
-#: taiga/projects/api.py:154 taiga/users/api.py:219
+#: taiga/projects/api.py:165 taiga/users/api.py:220
msgid "Incomplete arguments"
msgstr "不完整參數"
-#: taiga/projects/api.py:158 taiga/users/api.py:224
+#: taiga/projects/api.py:169 taiga/users/api.py:225
msgid "Invalid image format"
msgstr "無效的圖片檔案"
-#: taiga/projects/api.py:286
+#: taiga/projects/api.py:230
msgid "Not valid template name"
msgstr "非有效樣板名稱 "
-#: taiga/projects/api.py:289
+#: taiga/projects/api.py:233
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:356
+msgid "Invalid user id"
+msgstr ""
-#: taiga/projects/api.py:577
+#: taiga/projects/api.py:362
+msgid "The user doesn't exist"
+msgstr ""
+
+#: taiga/projects/api.py:366
+msgid "The user must be already a project member"
+msgstr ""
+
+#: 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:708
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: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
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/issues/models.py:52 taiga/projects/milestones/models.py:45
+#: taiga/projects/models.py:460 taiga/projects/models.py:486
+#: taiga/projects/models.py:517 taiga/projects/models.py:546
+#: taiga/projects/models.py:579 taiga/projects/models.py:602
+#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: taiga/projects/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 "專案"
-#: 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/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"
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/wiki/models.py:73 taiga/users/models.py:259
+#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
+#: taiga/projects/models.py:513 taiga/projects/models.py:540
+#: taiga/projects/models.py:575 taiga/projects/models.py:598
+#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
msgid "order"
msgstr "次序"
@@ -1395,18 +1422,34 @@ msgstr "自定"
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/custom_attributes/choices.py:26
+#: taiga/projects/choices.py:32
+msgid "This project is blocked due to payment failure"
+msgstr ""
+
+#: taiga/projects/choices.py:33
+msgid "This project is blocked by admin staff"
+msgstr ""
+
+#: taiga/projects/choices.py:34
+msgid "This project is 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"
@@ -1505,7 +1548,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 "無指定"
@@ -1566,23 +1609,23 @@ msgstr "封鎖筆記"
msgid "sprint"
msgstr "衝刺任務"
-#: taiga/projects/issues/api.py:163
+#: 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:167
+#: 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:171
+#: 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:175
+#: 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:179
+#: taiga/projects/issues/api.py:174
msgid "You don't have permissions to set this type to this issue."
msgstr "您無權限設定此問題的類型"
@@ -1636,27 +1679,27 @@ 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/wiki/models.py:32 taiga/users/models.py:253
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
+#: taiga/projects/models.py:474 taiga/projects/models.py:538
+#: taiga/projects/models.py:621 taiga/projects/models.py:679
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
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:483
-#: taiga/projects/models.py:547 taiga/projects/models.py:630
+#: 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"
@@ -1681,224 +1724,232 @@ msgstr "'{param}' 參數為必要"
msgid "'project' parameter is mandatory"
msgstr "'project'參數為必要"
-#: taiga/projects/models.py:95
+#: taiga/projects/models.py:78
msgid "email"
msgstr "電子郵件"
-#: taiga/projects/models.py:97
+#: taiga/projects/models.py:80
msgid "create at"
msgstr "創建於"
-#: taiga/projects/models.py:99 taiga/users/models.py:134
+#: taiga/projects/models.py:82 taiga/users/models.py:155
msgid "token"
msgstr "代號"
-#: taiga/projects/models.py:105
+#: taiga/projects/models.py:88
msgid "invitation extra text"
msgstr "額外文案邀請"
-#: taiga/projects/models.py:108
+#: taiga/projects/models.py:91
msgid "user order"
msgstr "使用者次序"
-#: taiga/projects/models.py:118
+#: taiga/projects/models.py:101
msgid "The user is already member of the project"
msgstr "使用者已是專案成員"
-#: taiga/projects/models.py:133
+#: taiga/projects/models.py:116
msgid "default points"
msgstr "預設點數"
-#: taiga/projects/models.py:137
+#: taiga/projects/models.py:120
msgid "default US status"
msgstr "預設使用者故事狀態"
-#: taiga/projects/models.py:141
+#: taiga/projects/models.py:124
msgid "default task status"
msgstr "預設任務狀態"
-#: taiga/projects/models.py:144
+#: taiga/projects/models.py:127
msgid "default priority"
msgstr "預設優先性"
-#: taiga/projects/models.py:147
+#: taiga/projects/models.py:130
msgid "default severity"
msgstr "預設嚴重性"
-#: taiga/projects/models.py:151
+#: taiga/projects/models.py:134
msgid "default issue status"
msgstr "預設問題狀態"
-#: taiga/projects/models.py:155
+#: taiga/projects/models.py:138
msgid "default issue type"
msgstr "預設議題類型"
-#: taiga/projects/models.py:171
+#: taiga/projects/models.py:154
msgid "logo"
msgstr "圖標"
-#: taiga/projects/models.py:181
+#: taiga/projects/models.py:164
msgid "members"
msgstr "成員"
-#: taiga/projects/models.py:184
+#: taiga/projects/models.py:167
msgid "total of milestones"
msgstr "全部里程碑"
-#: taiga/projects/models.py:185
+#: taiga/projects/models.py:168
msgid "total story points"
msgstr "全部故事點數"
-#: taiga/projects/models.py:188 taiga/projects/models.py:697
+#: taiga/projects/models.py:171 taiga/projects/models.py:692
msgid "active backlog panel"
msgstr "活躍的待辦任務優先表面板"
-#: taiga/projects/models.py:190 taiga/projects/models.py:699
+#: taiga/projects/models.py:173 taiga/projects/models.py:694
msgid "active kanban panel"
msgstr "活躍的看板式面板"
-#: taiga/projects/models.py:192 taiga/projects/models.py:701
+#: taiga/projects/models.py:175 taiga/projects/models.py:696
msgid "active wiki panel"
msgstr "活躍的維基面板"
-#: taiga/projects/models.py:194 taiga/projects/models.py:703
+#: taiga/projects/models.py:177 taiga/projects/models.py:698
msgid "active issues panel"
msgstr "活躍的問題面板"
-#: taiga/projects/models.py:197 taiga/projects/models.py:706
+#: taiga/projects/models.py:180 taiga/projects/models.py:701
msgid "videoconference system"
msgstr "視訊會議系統"
-#: taiga/projects/models.py:199 taiga/projects/models.py:708
+#: taiga/projects/models.py:182 taiga/projects/models.py:703
msgid "videoconference extra data"
msgstr "視訊會議額外資料"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:187
msgid "creation template"
msgstr "創建模版"
-#: taiga/projects/models.py:208
+#: taiga/projects/models.py:191
msgid "anonymous permissions"
msgstr "匿名權限"
-#: taiga/projects/models.py:212
+#: taiga/projects/models.py:195
msgid "user permissions"
msgstr "使用者權限"
-#: taiga/projects/models.py:215
+#: taiga/projects/models.py:198
msgid "is private"
msgstr "私密"
-#: taiga/projects/models.py:218
+#: taiga/projects/models.py:201
msgid "is featured"
msgstr " 受矚目的"
-#: taiga/projects/models.py:221
+#: taiga/projects/models.py:204
msgid "is looking for people"
msgstr "正在找人"
-#: taiga/projects/models.py:223
+#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:218
msgid "tags colors"
msgstr "標籤顏色"
-#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64
+#: taiga/projects/models.py:221
+msgid "project transfer token"
+msgstr ""
+
+#: 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:242 taiga/projects/models.py:254
+#: taiga/projects/models.py:232 taiga/projects/models.py:244
#: taiga/projects/votes/models.py:29
msgid "count"
msgstr "數量"
-#: taiga/projects/models.py:245
+#: taiga/projects/models.py:235
msgid "fans last week"
msgstr "上週粉絲"
-#: taiga/projects/models.py:248
+#: taiga/projects/models.py:238
msgid "fans last month"
msgstr "上個月粉絲"
-#: taiga/projects/models.py:251
+#: taiga/projects/models.py:241
msgid "fans last year"
msgstr "去年粉絲"
-#: taiga/projects/models.py:257
+#: taiga/projects/models.py:247
msgid "activity last week"
msgstr "上週活躍成員"
-#: taiga/projects/models.py:260
+#: taiga/projects/models.py:250
msgid "activity last month"
msgstr "上月活躍成員"
-#: taiga/projects/models.py:263
+#: taiga/projects/models.py:253
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:466
+#: 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:140
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,29 +1965,29 @@ 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 "已觀注"
-#: 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 "通知水平的無效值"
@@ -2682,54 +2733,63 @@ 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:172
msgid "Email address is already taken"
msgstr "電子郵件已使用"
-#: taiga/projects/serializers.py:249
+#: taiga/projects/serializers.py:184
msgid "Invalid role for the project"
msgstr "專案無效的角色"
-#: taiga/projects/serializers.py:425
+#: taiga/projects/serializers.py:195
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/serializers.py:198
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/serializers.py:394
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:426
+#: taiga/projects/serializers.py:395
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:427
+#: taiga/projects/serializers.py:396
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:428
+#: taiga/projects/serializers.py:397
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:429
+#: taiga/projects/serializers.py:398
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:430
+#: taiga/projects/serializers.py:399
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:431
+#: taiga/projects/serializers.py:400
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:432
+#: taiga/projects/serializers.py:401
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:433
+#: taiga/projects/serializers.py:402
msgid "Roles"
msgstr "角色"
-#: taiga/projects/services/stats.py:198
+#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "未來之衝刺"
@@ -2737,15 +2797,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 +2981,236 @@ 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
+#, 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\"."
+"p>\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(old_owner_name)s,\n"
+"%(new_owner_name)s has accepted your offer and will become the new project "
+"owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
+#, python-format
+msgid "%(new_owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
+msgid ""
+"\n"
+"From now on, your new status for this project will be \"admin\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18
+msgid ""
+"\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer accepted!\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+"
Hi %(owner_name)s,
\n"
+" %(rejecter_name)s has declined your offer and will not become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(rejecter_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
+msgid ""
+"\n"
+" If you want, you can still try to transfer the project ownership to a "
+"different person.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
+#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
+msgid "Request transfer to a different person"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(rejecter_name)s has declined your offer and will not become the new "
+"project owner for \"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
+#, python-format
+msgid "%(rejecter_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
+msgid ""
+"\n"
+"If you want, you can still try to transfer the project ownership to a "
+"different person.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
+msgid "Request transfer to a different person:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer declined\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(owner_name)s,
\n"
+" %(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
+msgid ""
+"\n"
+" Please, click on \"Continue\" if you would like to start the "
+"project transfer from the administration panel.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
+msgid "Continue"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"p>\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
+msgid "Accept or reject the project ownership transfer:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer offer\n"
+msgstr ""
+
#. Translators: Name of scrum project template.
#: taiga/projects/translations.py:29
msgid "Scrum"
@@ -3147,15 +3448,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 +3515,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'參數為必要"
@@ -3230,7 +3531,7 @@ msgstr "上次更改"
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:70
+#: taiga/timeline/signals.py:68
msgid "Check the history API for the exact diff"
msgstr "檢查API過去資料以找出差異"
@@ -3238,129 +3539,145 @@ 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:96
msgid "superuser status"
msgstr "超級使用者狀態 "
-#: taiga/users/models.py:76
+#: taiga/users/models.py:97
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "無經明確分派,即賦予該使用者所有權限,"
-#: taiga/users/models.py:106
+#: taiga/users/models.py:127
msgid "username"
msgstr "使用者名稱"
-#: taiga/users/models.py:107
+#: taiga/users/models.py:128
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "必填。最多30字元(可為數字,字母,符號....)"
-#: taiga/users/models.py:110
+#: taiga/users/models.py:131
msgid "Enter a valid username."
msgstr "輸入有效的使用者名稱 "
-#: taiga/users/models.py:113
+#: taiga/users/models.py:134
msgid "active"
msgstr "活躍"
-#: taiga/users/models.py:114
+#: 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:120
+#: taiga/users/models.py:141
msgid "biography"
msgstr "自傳"
-#: taiga/users/models.py:123
+#: taiga/users/models.py:144
msgid "photo"
msgstr "照片"
-#: taiga/users/models.py:124
+#: taiga/users/models.py:145
msgid "date joined"
msgstr "加入日期"
-#: taiga/users/models.py:126
+#: taiga/users/models.py:147
msgid "default language"
msgstr "預設語言 "
-#: taiga/users/models.py:128
+#: taiga/users/models.py:149
msgid "default theme"
msgstr "預設主題"
-#: taiga/users/models.py:130
+#: taiga/users/models.py:151
msgid "default timezone"
msgstr "預設時區"
-#: taiga/users/models.py:132
+#: taiga/users/models.py:153
msgid "colorize tags"
msgstr "顏色標籤"
-#: taiga/users/models.py:137
+#: taiga/users/models.py:158
msgid "email token"
msgstr "電子郵件符號 "
-#: taiga/users/models.py:139
+#: taiga/users/models.py:160
msgid "new email address"
msgstr "新電子郵件地址"
-#: taiga/users/models.py:256
+#: taiga/users/models.py:167
+msgid "max number of owned private projects"
+msgstr ""
+
+#: taiga/users/models.py:170
+msgid "max number of owned public projects"
+msgstr ""
+
+#: taiga/users/models.py:173
+msgid "max number of memberships for each owned private project"
+msgstr ""
+
+#: taiga/users/models.py:177
+msgid "max number of memberships for each owned public project"
+msgstr ""
+
+#: taiga/users/models.py:294
msgid "permissions"
msgstr "許可"
@@ -3372,10 +3689,26 @@ 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:602
+msgid "You can't have more private projects"
+msgstr ""
+
+#: taiga/users/services.py:612
+msgid "You can't have more public projects"
+msgstr ""
+
+#: taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/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/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/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 .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
def _get_user_project_membership(user, project):
- Membership = apps.get_model("projects", "Membership")
if user.is_anonymous():
return None
@@ -39,10 +38,17 @@ def _get_object_project(obj):
def is_project_owner(user, obj):
- """
- The owner attribute of a project is just an historical reference
- """
+ 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
@@ -51,7 +57,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
@@ -79,43 +85,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 c0b122f3..d32115c4 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
@@ -33,6 +34,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
@@ -50,6 +52,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
@@ -61,7 +64,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
@@ -88,6 +93,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)
@@ -97,9 +105,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
def get_queryset(self):
qs = super().get_queryset()
+ qs = qs.select_related("owner")
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
+ qs = qs.prefetch_related("memberships")
qs = qs.prefetch_related(Prefetch("notify_policies",
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
@@ -137,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
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
@@ -158,6 +168,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"])
@@ -171,7 +183,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"])
@@ -182,6 +194,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()
@@ -190,6 +203,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()
@@ -207,77 +221,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)
@@ -305,13 +248,161 @@ 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))
+
+ @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()
+ 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 a membership from the project
+ if not project.memberships.filter(user=user).exists():
+ return response.BadRequest(_("The user must be already a project member"))
+
+ reason = request.DATA.get('reason', None)
+ transfer_token = services.start_project_transfer(project, user, reason)
+ 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)
+
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ request.user,
+ project,
+ )
+ 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)
+ 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:
@@ -329,9 +420,15 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
def pre_save(self, obj):
if not obj.id:
obj.owner = self.request.user
- # TODO REFACTOR THIS
obj.template = self.request.QUERY_PARAMS.get('template', None)
+ # Validate if the owner have enought slots to create or update the project
+ # TODO: Move to the ProjectAdminSerializer
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(obj.owner, obj)
+ if not enough_slots:
+ members = max(obj.memberships.count(), 1)
+ raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error)
+
self._set_base_permissions(obj)
super().pre_save(obj)
@@ -342,10 +439,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
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()
@@ -365,7 +461,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,)
@@ -379,7 +477,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,)
@@ -393,7 +493,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,)
@@ -407,7 +509,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,)
@@ -421,7 +525,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,)
@@ -435,7 +540,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,)
@@ -449,7 +555,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,)
@@ -480,7 +587,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
@@ -490,17 +597,17 @@ class MembershipViewSet(ModelCrudViewSet):
def get_serializer_class(self):
use_admin_serializer = False
-
+
if self.action == "create":
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
@@ -518,10 +625,22 @@ 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.
+ if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
+ members = len(data["bulk_memberships"])
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ project.owner,
+ project,
+ members
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
+
try:
members = services.create_members_in_bulk(data["bulk_memberships"],
project=project,
@@ -539,15 +658,26 @@ 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()
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.id:
+ members = 1
+ (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ self.request.user,
+ obj.project,
+ members
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error)
+
if not obj.token:
obj.token = str(uuid.uuid1())
diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py
index 841b81d7..c8c56bb3 100644
--- a/taiga/projects/apps.py
+++ b/taiga/projects/apps.py
@@ -19,12 +19,11 @@ from django.apps import AppConfig
from django.apps import apps
from django.db.models import signals
-from . import signals as handlers
-
## Project Signals
def connect_projects_signals():
+ from . import signals as handlers
# On project object is created apply template.
signals.post_save.connect(handlers.project_post_save,
sender=apps.get_model("projects", "Project"),
@@ -51,6 +50,7 @@ def disconnect_projects_signals():
## Memberships Signals
def connect_memberships_signals():
+ from . import signals as handlers
# On membership object is deleted, update role-points relation.
signals.pre_delete.connect(handlers.membership_post_delete,
sender=apps.get_model("projects", "Membership"),
@@ -71,6 +71,7 @@ def disconnect_memberships_signals():
## US Statuses Signals
def connect_us_status_signals():
+ from . import signals as handlers
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
sender=apps.get_model("projects", "UserStoryStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
@@ -85,6 +86,7 @@ def disconnect_us_status_signals():
## Tasks Statuses Signals
def connect_task_status_signals():
+ from . import signals as handlers
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
sender=apps.get_model("projects", "TaskStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
diff --git a/taiga/projects/attachments/admin.py b/taiga/projects/attachments/admin.py
index b64613ed..ecfa6fed 100644
--- a/taiga/projects/attachments/admin.py
+++ b/taiga/projects/attachments/admin.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
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/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/attachments/models.py b/taiga/projects/attachments/models.py
index 40cbf611..daec4a2c 100644
--- a/taiga/projects/attachments/models.py
+++ b/taiga/projects/attachments/models.py
@@ -20,7 +20,7 @@ import hashlib
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.translation import ugettext_lazy as _
from django.utils.text import get_valid_filename
@@ -42,7 +42,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/choices.py b/taiga/projects/choices.py
index 2d929ec7..15234a6d 100644
--- a/taiga/projects/choices.py
+++ b/taiga/projects/choices.py
@@ -24,3 +24,12 @@ VIDEOCONFERENCES_CHOICES = (
("custom", _("Custom")),
("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 is blocked due to payment failure")),
+ (BLOCKED_BY_STAFF, _("This project is blocked by admin staff")),
+ (BLOCKED_BY_OWNER_LEAVING, _("This project is blocked because the owner left"))
+]
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/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/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/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/filters.py b/taiga/projects/filters.py
index 5868a6be..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)
@@ -70,7 +71,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/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/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/api.py b/taiga/projects/issues/api.py
index 59583ee5..c0ade2f4 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -16,24 +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
@@ -43,7 +40,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 +154,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 +174,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 +229,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/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/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/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/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/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/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 2a4e538a..f5c7b6ea 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -30,18 +30,18 @@ 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 *
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
@@ -94,11 +94,18 @@ 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)
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))
@@ -132,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()
@@ -146,7 +164,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:
@@ -159,7 +177,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:
@@ -188,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
@@ -281,6 +299,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 == URL_TYPE:
+ return self.sd.choice(URL_CHOICES)
return None
def create_bug(self, project):
@@ -449,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()
@@ -467,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()
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/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/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/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/migrations/0039_auto_20160322_1157.py b/taiga/projects/migrations/0039_auto_20160322_1157.py
new file mode 100644
index 00000000..51227138
--- /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 is blocked due to payment failure'), ('blocked-by-staff', 'This project is blocked by admin staff'), ('blocked-by-owner-leaving', 'This project is blocked because the owner left')], default=None, max_length=255, null=True, verbose_name='blocked code'),
+ ),
+ ]
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index 2823f762..1728362b 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,)
@@ -114,8 +116,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
'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..bf7bb469 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,10 @@ 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,63 +101,15 @@ 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):
- 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:
@@ -168,7 +117,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/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/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 672ce9ee..59d16f9a 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -18,6 +18,7 @@
import itertools
import uuid
+from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import signals, Q
@@ -27,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
@@ -69,7 +71,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,
@@ -215,6 +217,13 @@ 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"))
+
+ 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)
@@ -243,7 +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)
- _cached_user_stories = None
_importing = None
class Meta:
@@ -278,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
@@ -329,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()
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 726e639b..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
@@ -45,9 +39,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 +62,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 +70,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()
@@ -218,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)
@@ -233,13 +233,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)
@@ -266,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 ef56ace7..9c36fe75 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
@@ -32,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)
@@ -56,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"))
@@ -65,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,
@@ -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/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 1763ed7b..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
@@ -102,13 +100,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"]:
@@ -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/notifications/templates/emails/issues/issue-change-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja
index c07b9a92..47ce994d 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/updates-body-html.jinja" %}
{% block head %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
Issue updated
Hello {{ user }},
{{ changer }} has updated an issue on {{ project }}
Issue #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja
index adda2149..0f5fd94e 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja
@@ -1,6 +1,6 @@
{% extends "emails/updates-body-text.jinja" %}
{% block head %}
-{% trans user=user.get_full_name(), changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
Issue updated
Hello {{ user }}, {{ changer }} has updated an issue on {{ project }}
See issue #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja
index 707e2703..5cee74b7 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Updated the issue #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja
index 8147f59c..5560899a 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
New issue created
Hello {{ user }},
{{ changer }} has created a new issue on {{ project }}
Issue #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja
index 5b7ff367..3a69643c 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}
New issue created
Hello {{ user }}, {{ changer }} has created a new issue on {{ project }}
See issue #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja
index 7e4cf6bd..cde770ce 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Created the issue #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja
index 7b184627..1b20a020 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
Issue deleted
Hello {{ user }},
{{ changer }} has deleted an issue on {{ project }}
Issue #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja
index 7c39fc31..40c386f9 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
Issue deleted
Hello {{ user }}, {{ changer }} has deleted an issue on {{ project }}
Issue #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja
index bf297fa4..c5773908 100644
--- a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Deleted the issue #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja
index f756b38c..3e350766 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/updates-body-html.jinja" %}
{% block head %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
Sprint updated
Hello {{ user }},
{{ changer }} has updated an sprint on {{ project }}
Sprint {{ name }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja
index f98e35cb..dbb65b00 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja
@@ -1,6 +1,6 @@
{% extends "emails/updates-body-text.jinja" %}
{% block head %}
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("task", project.slug, snapshot.slug) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("task", project.slug, snapshot.slug) %}
Sprint updated
Hello {{ user }}, {{ changer }} has updated a sprint on {{ project }}
See sprint {{ name }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja
index 400bf944..1891a893 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, milestone=snapshot.name|safe %}
+{% trans project=project.name, milestone=snapshot.name %}
[{{ project }}] Updated the sprint "{{ milestone }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja
index 8390b282..39de7a4a 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
New sprint created
Hello {{ user }},
{{ changer }} has created a new sprint on {{ project }}
Sprint {{ name }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja
index 0433cf7f..8cbd531b 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
New sprint created
Hello {{ user }}, {{ changer }} has created a new sprint on {{ project }}
See sprint {{ name }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja
index 10656b83..700faac0 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, milestone=snapshot.name|safe %}
+{% trans project=project.name, milestone=snapshot.name %}
[{{ project }}] Created the sprint "{{ milestone }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja
index fcd2cb2d..4604ba41 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, name=snapshot.name|safe %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, name=snapshot.name %}
Sprint deleted
Hello {{ user }},
{{ changer }} has deleted an sprint on {{ project }}
Sprint {{ name }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja
index 2bb8bbe1..4c19f2cb 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name %}
Sprint deleted
Hello {{ user }}, {{ changer }} has deleted an sprint on {{ project }}
Sprint {{ name }}
diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja
index 11404786..242f1326 100644
--- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, milestone=snapshot.name|safe %}
+{% trans project=project.name, milestone=snapshot.name %}
[{{ project }}] Deleted the Sprint "{{ milestone }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja
index 024a41f2..83093f1f 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/updates-body-html.jinja" %}
{% block head %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}
Task updated
Hello {{ user }},
{{ changer }} has updated a task on {{ project }}
Task #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja
index f34a7d96..45c3015b 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja
@@ -1,6 +1,6 @@
{% extends "emails/updates-body-text.jinja" %}
{% block head %}
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}
Task updated
Hello {{ user }}, {{ changer }} has updated a task on {{ project }}
See task #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja
index 1e4d7c16..6275e839 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Updated the task #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja
index bd0e796e..496c77af 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}
New task created
Hello {{ user }},
{{ changer }} has created a new task on {{ project }}
Task #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja
index 34deae44..befa385c 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}
New task created
Hello {{ user }}, {{ changer }} has created a new task on {{ project }}
See task #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja
index d107e855..27dabde4 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Created the task #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja
index d10551cd..0eb1af1e 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
Task deleted
Hello {{ user }},
{{ changer }} has deleted a task on {{ project }}
Task #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja
index d2c11cdf..9234770b 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
Task deleted
Hello {{ user }}, {{ changer }} has deleted a task on {{ project }}
Task #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja
index e80e164f..8522f00c 100644
--- a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Deleted the task #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja
index 8cecace4..0ca026c2 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/updates-body-html.jinja" %}
{% block head %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
User Story updated
Hello {{ user }},
{{ changer }} has updated a user story on {{ project }}
User Story #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja
index 68719440..d9fc6d71 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja
@@ -1,6 +1,6 @@
{% extends "emails/updates-body-text.jinja" %}
{% block head %}
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe,ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name,ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
User story updated
Hello {{ user }}, {{ changer }} has updated a user story on {{ project }}
See user story #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja
index e116e403..3ed70685 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Updated the US #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja
index dc35ed3d..a1e4a8a1 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
New user story created
Hello {{ user }},
{{ changer }} has created a new user story on {{ project }}
User Story #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja
index 8bcd625c..842fc0d2 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}
New user story created
Hello {{ user }}, {{ changer }} has created a new user story on {{ project }}
See user story #{{ ref }} {{ subject }} at {{ url }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja
index 124e3060..1e166e57 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Created the US #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja
index 8decb3ef..1877b3b7 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
User Story deleted
Hello {{ user }},
{{ changer }} has deleted a user story on {{ project }}
User Story #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja
index 0158a1b8..b03e4879 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
User Story deleted
Hello {{ user }}, {{ changer }} has deleted a user story on {{ project }}
User Story #{{ ref }} {{ subject }}
diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja
index f7843217..3a48058e 100644
--- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
[{{ project }}] Deleted the US #{{ ref }} "{{ subject }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja
index 550c7ebe..f056254c 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/updates-body-html.jinja" %}
{% block head %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
Wiki Page updated
Hello {{ user }},
{{ changer }} has updated a wiki page on {{ project }}
Wiki page {{ page }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja
index 31f5884a..c20da951 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja
@@ -1,6 +1,6 @@
{% extends "emails/updates-body-text.jinja" %}
{% block head %}
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
Wiki Page updated
Hello {{ user }}, {{ changer }} has updated a wiki page on {{ project }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja
index 251a5adb..73912cb6 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, page=snapshot.slug %}
+{% trans project=project.name, page=snapshot.slug %}
[{{ project }}] Updated the Wiki Page "{{ page }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja
index 7627ef2b..28b1a0b5 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
New wiki page created
Hello {{ user }},
{{ changer }} has created a new wiki page on {{ project }}
Wiki page {{ page }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja
index 29d5e375..1a1e45af 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}
New wiki page created
Hello {{ user }}, {{ changer }} has created a new wiki page on {{ project }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja
index ac5a6ddf..6b7d1adf 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, page=snapshot.slug %}
+{% trans project=project.name, page=snapshot.slug %}
[{{ project }}] Created the Wiki Page "{{ page }}"
{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja
index 212441cf..291c1cb6 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja
@@ -1,7 +1,7 @@
{% extends "emails/base-body-html.jinja" %}
{% block body %}
- {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug %}
Wiki page deleted
Hello {{ user }},
{{ changer }} has deleted a wiki page on {{ project }}
Wiki page {{ page }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja
index a53b87a5..64745aed 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja
@@ -1,4 +1,4 @@
-{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug %}
Wiki page deleted
Hello {{ user }}, {{ changer }} has deleted a wiki page on {{ project }}
diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja
index d73de78f..a807f6f3 100644
--- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja
+++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=project.name|safe, page=snapshot.slug %}
+{% trans project=project.name, page=snapshot.slug %}
[{{ project }}] Deleted the Wiki Page "{{ page }}"
{% endtrans %}
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index c1e017a7..de65cd69 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
@@ -37,30 +37,38 @@ 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
+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')
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')
@@ -69,17 +77,22 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
+ transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ transfer_request_perms = IsProjectAdmin()
+ transfer_start_perms = IsMainOwner()
+ transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
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')
@@ -87,89 +100,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/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/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 04bebd11..829eec89 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, is_project_owner
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from . import models
@@ -76,7 +76,6 @@ class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
-
class Meta:
model = models.TaskStatus
i18n_fields = ("name",)
@@ -130,6 +129,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 +147,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:
@@ -181,15 +185,17 @@ 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_owners(project, exclude_user=self.object.user)):
- raise serializers.ValidationError(_("At least one of the user must be an active admin"))
+ if (self.object and self.object.user):
+ if self.object.user.id == project.owner_id and attrs[source] != True:
+ raise serializers.ValidationError(_("The project owner must be admin."))
+
+ if not services.project_has_valid_admins(project, exclude_user=self.object.user):
+ raise serializers.ValidationError(_("At least one user must be an active admin for this project."))
return attrs
@@ -242,7 +248,10 @@ 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")
tags = TagsField(default=[], required=False)
@@ -257,9 +266,10 @@ 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", "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:
@@ -271,6 +281,11 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return is_project_owner(self.context["request"].user, obj)
return False
+ def get_i_am_admin(self, obj):
+ if "request" in self.context:
+ return is_project_admin(self.context["request"].user, obj)
+ return False
+
def get_i_am_member(self, obj):
if "request" in self.context:
user = self.context["request"].user
@@ -328,6 +343,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)
@@ -337,13 +353,27 @@ 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")
+
class Meta:
model = models.Project
- read_only_fields = ("created_date", "modified_date", "owner", "slug")
+ 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)
+
+
+
######################################################
## Liked
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index d4940c37..6227cc54 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -37,7 +37,9 @@ 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 .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
@@ -46,3 +48,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/members.py b/taiga/projects/services/members.py
index f4efc5e9..f408d430 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -36,23 +36,87 @@ 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_admin=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):
membership = project.memberships.get(user=user)
- if not membership.is_owner:
+ if not membership.is_admin:
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
+
+
+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()
+
+
+ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships'
+ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships'
+ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
+ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
+
+def check_if_project_privacity_can_be_changed(project):
+ """Return if the project privacity can be changed from private to public or viceversa.
+
+ :param project: A project object.
+
+ :return: 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_PUBLIC_PROJECTS_MEMBERSHIPS
+
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ max_projects = project.owner.max_public_projects
+ error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
+ else:
+ current_memberships = project.memberships.count()
+ max_memberships = project.owner.max_memberships_private_projects
+ error_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
+
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ max_projects = project.owner.max_private_projects
+ error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return {'can_be_updated': False, 'reason': error_members_exceeded}
+
+ if max_projects is not None and current_projects >= max_projects:
+ return {'can_be_updated': False, 'reason': error_project_exceeded}
+
+ return {'can_be_updated': True, 'reason': None}
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/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/services/transfer.py b/taiga/projects/services/transfer.py
new file mode 100644
index 00000000..bef18577
--- /dev/null
+++ b/taiga/projects/services/transfer.py
@@ -0,0 +1,114 @@
+# 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(user, context)
+ email.send()
+
+
+def validate_project_transfer_token(token, project, user):
+ signer = signing.TimestampSigner()
+
+ if project.transfer_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_project_transfer_token(token, project, user)
+
+ 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_project_transfer_token(token, project, user)
+
+ # 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,
+ "old_owner": old_owner,
+ "new_owner": user,
+ "reason": reason
+ }
+ email = template(old_owner, context)
+ email.send()
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/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/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/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/templates/emails/membership_invitation-subject.jinja b/taiga/projects/templates/emails/membership_invitation-subject.jinja
index 0b5206ef..8e620317 100644
--- a/taiga/projects/templates/emails/membership_invitation-subject.jinja
+++ b/taiga/projects/templates/emails/membership_invitation-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=membership.project|safe %}
+{% trans project=membership.project %}
[Taiga] Invitation to join to the project '{{ project }}'
{% endtrans %}
diff --git a/taiga/projects/templates/emails/membership_notification-subject.jinja b/taiga/projects/templates/emails/membership_notification-subject.jinja
index 57d60ac6..c6bdd588 100644
--- a/taiga/projects/templates/emails/membership_notification-subject.jinja
+++ b/taiga/projects/templates/emails/membership_notification-subject.jinja
@@ -1,3 +1,3 @@
-{% trans project=membership.project|safe %}
+{% trans project=membership.project %}
[Taiga] Added to the project '{{ project }}'
{% endtrans %}
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..6c8f409e
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_accept-body-html.jinja
@@ -0,0 +1,17 @@
+{% 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 %}
+ 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 %}
+
+ {% trans %}
+ 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
new file mode 100644
index 00000000..0fda1124
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_accept-body-text.jinja
@@ -0,0 +1,18 @@
+{% 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 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".
+{% 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..982915f7
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja
@@ -0,0 +1,24 @@
+{% 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 %}
+ Hi {{ owner_name }},
+ {{ rejecter_name }} has declined your offer and will not become the new project owner for "{{project_name}}".
+ {% endtrans %}
+
+ {% if reason %}
+ {% 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.
+ {% endtrans %}
+
+
+ {% trans %}Request transfer to a different person{% 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..972cce54
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_reject-body-text.jinja
@@ -0,0 +1,21 @@
+{% 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 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.
+{% 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..3351c382
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_request-body-html.jinja
@@ -0,0 +1,15 @@
+{% 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 %}
+ 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 %}
+{% 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..06cb70a6
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_start-body-html.jinja
@@ -0,0 +1,23 @@
+{% 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 %}
+ 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 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.
+ {% endtrans %}
+
+ {{ _("Continue") }}
+{% 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..1c24ed20
--- /dev/null
+++ b/taiga/projects/templates/emails/transfer_start-body-text.jinja
@@ -0,0 +1,20 @@
+{% 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 owner_name=project.owner.get_full_name() %}{{ owner_name }} says:{% endtrans %}
+
+{{ reason }}
+{% endif %}
+
+{% trans %}
+Please, go to the following link to either accept or reject this proposal.
+{% endtrans %}
+
+{{ _("Accept or reject the project ownership 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/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/userstories/apps.py b/taiga/projects/userstories/apps.py
index 4ef7d996..58e0a65c 100644
--- a/taiga/projects/userstories/apps.py
+++ b/taiga/projects/userstories/apps.py
@@ -82,27 +82,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/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/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/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/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/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/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/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/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 898e3aa9..c9df776d 100644
--- a/taiga/timeline/apps.py
+++ b/taiga/timeline/apps.py
@@ -17,21 +17,23 @@
from django.apps import AppConfig
from django.apps import apps
+from django.contrib.auth import get_user_model
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,
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/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/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/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)),
]
diff --git a/taiga/users/admin.py b/taiga/users/admin.py
index 729d64bf..9d4c9815 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',)}),
- (_('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_memberships_private_projects"),
+ ("max_public_projects", "max_memberships_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/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/users/migrations/0015_auto_20160120_1409.py b/taiga/users/migrations/0015_auto_20160120_1409.py
new file mode 100644
index 00000000..0cfb0ddc
--- /dev/null
+++ b/taiga/users/migrations/0015_auto_20160120_1409.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', '0014_auto_20151005_1357'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='max_private_projects',
+ field=models.IntegerField(null=True, verbose_name='max number of owned private projects', 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 owned public projects', 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..244fcd00
--- /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_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_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/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'},
+ ),
+ ]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index dee7e1a0..e205e583 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -15,18 +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
@@ -35,9 +39,45 @@ from taiga.auth.tokens import get_token_for_user
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.files import get_file_path
from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.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():
@@ -51,11 +91,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
@@ -84,25 +124,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="",
@@ -117,16 +157,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 owned private projects"))
+ max_public_projects = models.IntegerField(null=True, blank=True,
+ default=settings.MAX_PUBLIC_PROJECTS_PER_USER,
+ verbose_name=_("max number of owned public projects"))
+ 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
_cached_watched_ids = None
_cached_notify_levels = None
- USERNAME_FIELD = 'username'
- REQUIRED_FIELDS = ['email']
+ USERNAME_FIELD = "username"
+ REQUIRED_FIELDS = ["email"]
objects = UserManager()
@@ -134,9 +191,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()
@@ -226,6 +280,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,
@@ -256,16 +313,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(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/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/users/serializers.py b/taiga/users/serializers.py
index 5b381252..d36ba768 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -104,20 +104,36 @@ 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",
+ "max_memberships_private_projects", "max_memberships_public_projects",
+ "total_private_projects", "total_public_projects")
+
+ read_only_fields = ("id", "email",
+ "max_private_projects", "max_public_projects",
+ "max_memberships_private_projects",
+ "max_memberships_public_projects")
+
+ 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):
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):
@@ -182,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.CharField()
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 55c82ca1..b18e8b36 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))
@@ -123,7 +124,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
@@ -322,7 +323,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 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 +418,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 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 +501,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 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}
@@ -572,3 +573,62 @@ 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, project, new_members=0):
+ # TODO: Refactor: Create one service for every type of action and move to project services
+ #
+ # - has_available_slot_to_create_new_project()
+ # - has_available_slot_to_update_this_project()
+ # - has_available_slot_to_transfer_this_project()
+ # - has_available_slot_to_import_this_project()
+ # - has_available_slot_to_add_members_to_this_project()
+
+ (enough, error) = _has_available_slot_for_project_type(user, project)
+ if not enough:
+ return (enough, error)
+ return _has_available_slot_for_project_members(user, project, new_members)
+
+
+def _has_available_slot_for_project_type(user, project):
+ if project.is_private:
+ if user.max_private_projects is None:
+ return (True, None)
+
+ current_private_projects = user.owned_projects.filter(is_private=True).exclude(id=project.id).count()
+ if current_private_projects < user.max_private_projects:
+ return (True, None)
+
+ return (False, _("You can't have more private projects"))
+
+ else:
+ if user.max_public_projects is None:
+ return (True, None)
+
+ current_public_project = user.owned_projects.filter(is_private=False).exclude(id=project.id).count()
+ if current_public_project < user.max_public_projects:
+ return (True, None)
+
+ return (False, _("You can't have more public projects"))
+
+
+def _has_available_slot_for_project_members(user, project, new_members):
+ current_memberships = max(project.memberships.count(), 1)
+
+ if project.is_private:
+ if user.max_memberships_private_projects is None:
+ return (True, None)
+
+ if current_memberships + new_members <= user.max_memberships_private_projects:
+ return (True, None)
+
+ return (False, _("You have reached your current limit of memberships for private projects"))
+
+ else:
+ if user.max_memberships_public_projects is None:
+ return (True, None)
+
+ if current_memberships + new_members <= user.max_memberships_public_projects:
+ return (True, None)
+
+ return (False, _("You have reached your current limit of memberships for public projects"))
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/taiga/webhooks/apps.py b/taiga/webhooks/apps.py
index f6dda872..a10cd1d2 100644
--- a/taiga/webhooks/apps.py
+++ b/taiga/webhooks/apps.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 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():
- 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/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()
diff --git a/tests/factories.py b/tests/factories.py
index d5d6d06b..252ce47a 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:
@@ -162,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))
@@ -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/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index 520e266a..0395d8b4 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,19 +74,30 @@ 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,
- 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_admin=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_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 da0fa833..2c90cbfc 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,21 +87,37 @@ 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)
+ 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_admin=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..2a6d3974 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,18 +79,30 @@ 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,
- 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_admin=True)
m.public_issue = f.IssueFactory(project=m.public_project,
status__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..5a6f26a4 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,22 +71,35 @@ 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,
- 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_admin=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..8260bd2f
--- /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_admin=True)
+
+ f.MembershipFactory(project=m.private_project1,
+ user=m.project_owner,
+ is_admin=True)
+
+ f.MembershipFactory(project=m.private_project2,
+ user=m.project_owner,
+ is_admin=True)
+
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_admin=True)
+
+ 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..207889f9 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,46 +67,65 @@ 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,
- 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_admin=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 81551b38..9bcd7a9f 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,18 +58,30 @@ 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,
- 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_admin=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,6 +573,8 @@ 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]
def test_project_list_with_discover_mode_enabled(client, data):
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 05b5f49d..1fd33e46 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,21 +87,37 @@ 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)
+ 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_admin=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..4771d12c 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,22 +79,35 @@ 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,
- 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_admin=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_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 a665e566..9e6bd6ff 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,22 +88,37 @@ 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)
+ 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_admin=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..80559a87 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,22 +79,35 @@ 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,
- 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_admin=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..5712b5bc 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)
+ is_admin=True)
+ f.MembershipFactory(project=m.blocked_project,
+ user=m.project_owner,
+ is_admin=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..c69fb2bc 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,26 +73,40 @@ 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,
- 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_admin=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_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 a50b02ca..4494c9f5 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
@@ -102,6 +103,26 @@ def test_ok_signature_invalid_network(client):
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={
+ "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={
@@ -324,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()
@@ -530,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,))
@@ -545,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 5b832643..9bede876 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")
@@ -241,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()
@@ -438,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,))
@@ -453,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 0f5ee616..14156b37 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
@@ -118,6 +119,27 @@ def test_ok_signature_invalid_network(client):
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={
+ "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={
@@ -386,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()
@@ -594,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,))
@@ -609,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 c8573055..159abb76 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,88 @@ 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 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):
+ 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 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
+
+
+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))
+
+ 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)
@@ -280,7 +363,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])
@@ -293,7 +376,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)
@@ -314,7 +397,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)
@@ -338,7 +421,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)
@@ -361,7 +444,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)
@@ -389,7 +472,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)
@@ -425,7 +508,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)
@@ -450,7 +533,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)
@@ -510,7 +593,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])
@@ -523,7 +606,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)
@@ -544,7 +627,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)
@@ -575,7 +658,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)
@@ -597,7 +680,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)
@@ -618,7 +701,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])
@@ -631,7 +714,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)
@@ -651,7 +734,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)
@@ -676,7 +759,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)
@@ -707,7 +790,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)
@@ -729,7 +812,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)
@@ -750,7 +833,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()
@@ -771,7 +854,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])
@@ -784,7 +867,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])
@@ -802,7 +885,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])
@@ -830,7 +913,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])
@@ -850,7 +933,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])
@@ -863,7 +946,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])
@@ -881,7 +964,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])
@@ -895,7 +978,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])
@@ -913,7 +996,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])
@@ -930,6 +1013,92 @@ 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_projects_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 private projects" in str(excinfo.value)
+
+
+def test_dict_to_project_with_no_members_private_project_slots_available(client):
+ user = f.UserFactory.create(max_memberships_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 your current 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_memberships_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 your current limit of memberships for public" in str(excinfo.value)
+
+
def test_invalid_dump_import(client):
user = f.UserFactory.create()
client.login(user)
@@ -957,6 +1126,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")
@@ -986,7 +1156,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 +1179,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 +1200,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 +1224,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"
@@ -1061,6 +1235,256 @@ def test_dump_import_throttling(client, settings):
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 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
+
+
+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 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
+
+
+def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client):
+ user = f.UserFactory.create(max_memberships_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 your current 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_memberships_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 your current 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_memberships_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_memberships_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"
+
+
def test_valid_dump_import_without_slug(client):
project = f.ProjectFactory.create(slug="existing-slug")
user = f.UserFactory.create()
@@ -1077,3 +1501,87 @@ def test_valid_dump_import_without_slug(client):
response = client.post(url, {'dump': data})
assert response.status_code == 201
+
+
+def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_project(client):
+ user = f.UserFactory.create(max_memberships_private_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "private-project-with-memberships-limit-with-you",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True,
+ "memberships": [
+ {
+ "email": user.email,
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ ],
+ "roles": [{"name": "Role"}]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ assert Project.objects.filter(slug="private-project-with-memberships-limit-with-you").count() == 1
+
+
+def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_project(client):
+ user = f.UserFactory.create(max_memberships_public_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "public-project-with-memberships-limit-with-you",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "memberships": [
+ {
+ "email": user.email,
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ ],
+ "roles": [{"name": "Role"}]
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ assert Project.objects.filter(slug="public-project-with-memberships-limit-with-you").count() == 1
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 75d7f29a..c878bd45 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")
@@ -53,10 +53,143 @@ 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_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)
+
+ 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 your current 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 your current 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)
+ role = f.RoleFactory(project=project, name="Test")
+ f.MembershipFactory(project=project, user=user, is_admin=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_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)
+
+ 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 your current 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_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)
+
+ 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")
- 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"
@@ -81,7 +214,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)
@@ -96,7 +229,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)
@@ -149,7 +282,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()
@@ -162,8 +295,78 @@ 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_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)
+
+ 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 your current 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_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)
+
+ 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_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)
+
+ 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 your current 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_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)
+
+ 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)
+ membership = f.MembershipFactory(is_admin=True)
client.login(membership.user)
url = reverse("memberships-detail", args=[membership.id])
data = {"email": "new@email.com"}
@@ -171,16 +374,30 @@ 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_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)
@@ -189,7 +406,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 c78ab048..85ea89fb 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -712,7 +712,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)
@@ -750,7 +750,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)
@@ -802,7 +802,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)
@@ -854,7 +854,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 e3c80617..b086f402 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()
@@ -43,9 +56,156 @@ 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 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):
+ 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 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):
+ project = f.create_project(is_private=True, owner__max_public_projects=0)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=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 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):
+ project = f.create_project(is_private=False, owner__max_private_projects=0)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=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 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):
+ 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_admin=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_admin=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_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_admin=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)
+ f.MembershipFactory(user=project.owner, project=project, is_admin=True)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {"name": ""}
@@ -96,7 +256,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)
@@ -116,7 +276,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)
@@ -136,7 +296,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)
@@ -157,7 +317,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})
@@ -244,12 +404,27 @@ 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)
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_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,))
+ 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):
@@ -281,34 +456,49 @@ 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)
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_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,))
+ 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):
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] == "At least one of the user must be an active admin"
+ assert response.data["is_admin"][0] == "At least one user must be an active admin for this project."
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})
@@ -322,7 +512,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()
@@ -367,7 +557,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})
@@ -393,11 +583,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
@@ -547,3 +737,890 @@ 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
+
+
+####################################################################################
+# Test transfer project ownership
+####################################################################################
+
+
+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.create_membership(user=user, project=project, role=role, is_admin=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.create_membership(user=user, project=project, role=role, is_admin=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.create_membership(user=user_from, project=project, is_admin=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_a_not_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)
+
+ 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_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()
+
+ 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-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_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.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})
+
+ 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_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.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})
+
+ 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_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.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})
+
+ 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.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})
+
+ 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_from.id
+ assert project.transfer_token is None
+
+
+def test_project_transfer_reject_from_no_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.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})
+
+ 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_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.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})
+
+ 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_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.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})
+
+ 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_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.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})
+
+ 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_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.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})
+
+ 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_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)
+
+ signer = signing.TimestampSigner()
+ token = signer.sign(user_to.id)
+ project = f.create_project(owner=user_from, transfer_token=token, is_private=False)
+
+ f.create_membership(user=user_from, project=project, is_admin=True)
+ f.create_membership(user=user_to, 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})
+
+ 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_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)
+
+ 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)
+
+ 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})
+
+ 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.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})
+
+ 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
+
+
+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()
+
+ 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-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_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.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})
+
+ 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_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.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})
+
+ 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_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.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})
+
+ 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.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})
+
+ data = {
+ "token": token,
+ }
+
+ 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.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
+
+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) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS})
+
+
+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) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS})
+
+
+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) ==
+ {'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):
+ 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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
+
+
+# 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) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS})
+
+
+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) ==
+ {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS})
+
+
+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) ==
+ {'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):
+ 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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
+
+
+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) == {'can_be_updated': True, 'reason': None})
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 6b46983b..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()
@@ -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 88b29523..360c6054 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')
@@ -214,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
@@ -487,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
@@ -546,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
@@ -599,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"] != ""
@@ -655,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"] != ""
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 f387474d..ca3414e6 100644
--- a/tests/integration/test_vote_tasks.py
+++ b/tests/integration/test_vote_tasks.py
@@ -26,8 +26,8 @@ pytestmark = pytest.mark.django_db
def test_upvote_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-upvote", args=(task.id,))
client.login(user)
@@ -38,8 +38,8 @@ def test_upvote_task(client):
def test_downvote_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ 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)
@@ -93,8 +93,8 @@ def test_get_task_votes(client):
def test_get_task_is_voted(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ 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 772937b8..b8caa01b 100644
--- a/tests/integration/test_vote_userstories.py
+++ b/tests/integration/test_vote_userstories.py
@@ -26,8 +26,8 @@ pytestmark = pytest.mark.django_db
def test_upvote_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-upvote", args=(user_story.id,))
client.login(user)
@@ -38,8 +38,8 @@ def test_upvote_user_story(client):
def test_downvote_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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)
@@ -92,8 +92,8 @@ 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)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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 c5010827..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))
@@ -79,8 +79,8 @@ def test_get_issue_watcher(client):
def test_get_issue_watchers(client):
user = f.UserFactory.create()
- issue = f.IssueFactory(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ issue = f.create_issue(owner=user)
+ 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)
@@ -95,8 +95,8 @@ def test_get_issue_watchers(client):
def test_get_issue_is_watcher(client):
user = f.UserFactory.create()
- issue = f.IssueFactory(owner=user)
- f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
+ issue = f.create_issue(owner=user)
+ 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 cde26ed0..38ddd40b 100644
--- a/tests/integration/test_watch_tasks.py
+++ b/tests/integration/test_watch_tasks.py
@@ -27,8 +27,8 @@ pytestmark = pytest.mark.django_db
def test_watch_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ f.MembershipFactory.create(project=task.project, user=user, is_admin=True)
url = reverse("tasks-watch", args=(task.id,))
client.login(user)
@@ -39,8 +39,8 @@ def test_watch_task(client):
def test_unwatch_task(client):
user = f.UserFactory.create()
- task = f.create_task(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ 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)
@@ -95,8 +95,8 @@ def test_get_task_watchers(client):
def test_get_task_is_watcher(client):
user = f.UserFactory.create()
- task = f.TaskFactory(owner=user)
- f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
+ task = f.create_task(owner=user, milestone=None)
+ 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 e17081cd..66ae4e0c 100644
--- a/tests/integration/test_watch_userstories.py
+++ b/tests/integration/test_watch_userstories.py
@@ -27,8 +27,8 @@ pytestmark = pytest.mark.django_db
def test_watch_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True)
url = reverse("userstories-watch", args=(user_story.id,))
client.login(user)
@@ -39,8 +39,8 @@ def test_watch_user_story(client):
def test_unwatch_user_story(client):
user = f.UserFactory.create()
- user_story = f.create_userstory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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,))
@@ -65,8 +65,8 @@ def test_list_user_story_watchers(client):
def test_get_user_story_watcher(client):
user = f.UserFactory.create()
- user_story = f.UserStoryFactory(owner=user)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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))
@@ -79,8 +79,8 @@ def test_get_user_story_watcher(client):
def test_get_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)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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)
@@ -95,8 +95,8 @@ 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)
- f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
+ user_story = f.create_userstory(owner=user, status=None)
+ 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,))
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()
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